Home > Software design >  Putting json.loads response inside of dataclasses
Putting json.loads response inside of dataclasses

Time:12-18

I created a couple of dataclasses (similar to Go's structs) and I want to put my response/data inside of the dataclass. I haven't been able to find wether or not the json package supports this out of the box.

Dataclasses

from dataclasses import dataclass
from typing import List


@dataclass
class TakerPays:
    currency: str
    issuer: str
    value: str


@dataclass
class Offers:
    account: str
    book_directory: str
    book_node: str
    flags: int
    ledger_entry_type: str
    owner_node: str
    previous_tx_id: str
    previous_tx_lgr_seq: int
    sequence: int
    taker_gets: str
    taker_pays: TakerPays
    index: str
    owner_funds: str
    quality: str


@dataclass
class Warnings:
    id: int
    message: str


@dataclass
class Result:
    ledger_hash: str
    ledger_index: int
    offers: List[Offers]
    validated: bool
    warnings: List[Warnings]


@dataclass
class Response:
    id: int
    result: Result
    status: str
    type: str

Preview of the json that needs to be put into the Response dataclass

{
  "id": 4,
  "result": {
    "ledger_hash": "5848C7DB5024EC3B532AC2F93BA8086A3D6281D3C0746BFE62E7E3CF4853F663",
    "ledger_index": 68379996,
    "offers": [
      {
        "Account": "rPbMHxs7vy5t6e19tYfqG7XJ6Fog8EPZLk",
        "BookDirectory": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E1D157637A1048F",
        "BookNode": "0",
        "Flags": 0,
        "LedgerEntryType": "Offer",
        "OwnerNode": "0",
        "PreviousTxnID": "72B8928E31DF89223C7ADE0030685289BAD772C72DF23DDFFB92FF7B48BAC622",
        "PreviousTxnLgrSeq": 68379985,
        "Sequence": 386826,
        "TakerGets": "789784836",
        "TakerPays": {
          "currency": "USD",
          "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
          "value": "646.5472316"
        },
        "index": "82F565EDEF8661D7D9C92A75E2F0F5DBF2BAAFAE96A5A5A768AD76B933016031",
        "owner_funds": "4587408572",
        "quality": "0.0000008186371808232591"
      }
    ],
    "validated": true,
    "warnings": [
      {
        "id": 1004,
        "message": "This is a reporting server.  The default behavior of a reporting server is to only return validated data. If you are looking for not yet validated data, include \"ledger_index : current\" in your request, which will cause this server to forward the request to a p2p node. If the forward is successful the response will include \"forwarded\" : \"true\""
      }
    ]
  },
  "status": "success",
  "type": "response"
}

I've not been able to find support to put the json into the dataclass similar to Go's json.Unmarshal and I'm curious to the best way to do this

CodePudding user response:

Unfortunately the builtin modules in Python such as json don't support de-serializing JSON into a nested dataclass model as in this case.

If you're on board with using third-party libraries, a solid option is to leverage the dataclass-wizard library for this task, as shown below; one advantage that it offers - which really helps in this particular example - is auto key casing transforms, such as from "camelCase" and "TitleCase".

Note that if you prefer not to subclass from any Mixin class, you can opt to use the helper functions fromdict and asdict to convert data from/to Python dict objects instead.

from dataclasses import dataclass
from typing import List

from dataclass_wizard import JSONWizard


@dataclass
class TakerPays:
    currency: str
    issuer: str
    value: str


@dataclass
class Offers:
    account: str
    book_directory: str
    book_node: str
    flags: int
    ledger_entry_type: str
    owner_node: str
    previous_txn_id: str
    previous_txn_lgr_seq: int
    sequence: int
    taker_gets: str
    taker_pays: TakerPays
    index: str
    owner_funds: str
    quality: str


@dataclass
class Warnings:
    id: int
    message: str


@dataclass
class Result:
    ledger_hash: str
    ledger_index: int
    offers: List[Offers]
    validated: bool
    warnings: List[Warnings]


@dataclass
class Response(JSONWizard):
    id: int
    result: Result
    status: str
    type: str


def main():
    string = r"""
    {
      "id": 4,
      "result": {
        "ledger_hash": "5848C7DB5024EC3B532AC2F93BA8086A3D6281D3C0746BFE62E7E3CF4853F663",
        "ledger_index": 68379996,
        "offers": [
          {
            "Account": "rPbMHxs7vy5t6e19tYfqG7XJ6Fog8EPZLk",
            "BookDirectory": "DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E1D157637A1048F",
            "BookNode": "0",
            "Flags": 0,
            "LedgerEntryType": "Offer",
            "OwnerNode": "0",
            "PreviousTxnID": "72B8928E31DF89223C7ADE0030685289BAD772C72DF23DDFFB92FF7B48BAC622",
            "PreviousTxnLgrSeq": 68379985,
            "Sequence": 386826,
            "TakerGets": "789784836",
            "TakerPays": {
              "currency": "USD",
              "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B",
              "value": "646.5472316"
            },
            "index": "82F565EDEF8661D7D9C92A75E2F0F5DBF2BAAFAE96A5A5A768AD76B933016031",
            "owner_funds": "4587408572",
            "quality": "0.0000008186371808232591"
          }
        ],
        "validated": true,
        "warnings": [
          {
            "id": 1004,
            "message": "This is a reporting server.  The default behavior of a reporting server is to only return validated data. If you are looking for not yet validated data, include \"ledger_index : current\" in your request, which will cause this server to forward the request to a p2p node. If the forward is successful the response will include \"forwarded\" : \"true\""
          }
        ]
      },
      "status": "success",
      "type": "response"
    }
    """

    r = Response.from_json(string)
    print(repr(r))


if __name__ == '__main__':
    main()

Output:

Response(id=4, result=Result(ledger_hash='5848C7DB5024EC3B532AC2F93BA8086A3D6281D3C0746BFE62E7E3CF4853F663', ledger_index=68379996, offers=[Offers(account='rPbMHxs7vy5t6e19tYfqG7XJ6Fog8EPZLk', book_directory='DFA3B6DDAB58C7E8E5D944E736DA4B7046C30E4F460FD9DE4E1D157637A1048F', book_node='0', flags=0, ledger_entry_type='Offer', owner_node='0', previous_txn_id='72B8928E31DF89223C7ADE0030685289BAD772C72DF23DDFFB92FF7B48BAC622', previous_txn_lgr_seq=68379985, sequence=386826, taker_gets='789784836', taker_pays=TakerPays(currency='USD', issuer='rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', value='646.5472316'), index='82F565EDEF8661D7D9C92A75E2F0F5DBF2BAAFAE96A5A5A768AD76B933016031', owner_funds='4587408572', quality='0.0000008186371808232591')], validated=True, warnings=[Warnings(id=1004, message='This is a reporting server.  The default behavior of a reporting server is to only return validated data. If you are looking for not yet validated data, include "ledger_index : current" in your request, which will cause this server to forward the request to a p2p node. If the forward is successful the response will include "forwarded" : "true"')]), status='success', type='response')

NB: I noted that two fields in the Offers dataclass have slightly different names than the fields in the JSON object. For example, the field previous_tx_id is associated with the key PreviousTxnID in the JSON object.

Assuming this was intentional, you could easily work around this by defining a field alias mapping, as shown below:

from dataclass_wizard import json_key
# Note: In Python 3.9  you can import this from `typing` instead
from typing_extensions import Annotated  

@dataclass
class Offers:
    ...
    previous_tx_id: Annotated[str, json_key('PreviousTxnID')]
    previous_tx_lgr_seq: Annotated[int, json_key('PreviousTxnLgrSeq')]
    ...
  • Related