Home > Net >  deserialize json with objects
deserialize json with objects

Time:11-04

In python 3, how can I deserialize an object structure from json?

Example json:

{ 'name': 'foo',
  'some_object': { 'field1': 'bar', 'field2' : '0' },
  'some_list_of_objects': [
      { 'field1': 'bar1', 'field2' : '1' },
      { 'field1': 'bar2', 'field2' : '2' }, 
      { 'field1': 'bar3', 'field2' : '3' },
  ]
}

Here's my python code:

import json

class A:
  name: str
  some_object: B
  some_list_of_objects: list(C)

  def __init__(self, file_name):
    with open(file_name, "r") as json_file:
        self.__dict__ = json.load(json_file)

class B:
  field1: int
  field2: str

class C:
  field1: int
  field2: str

How to force some_object to be of type B and some_list_of_objects to be of type list of C?

CodePudding user response:

As you're using Python 3, I would suggest using dataclasses to model your classes. This should improve your overall code quality and also eliminate the need to explicltly declare an __init__ constructor method for your class, for example.

If you're on board with using a third-party library, I'd suggest looking into an efficient JSON serialization library like the dataclass-wizard that performs implicit type conversion - for example, string to annotated int as below. Note that I'm using StringIO here, which is a file-like object containing a JSON string to de-serialize into a nested class model.

Note: the following approach should work in Python 3.7 .

from __future__ import annotations

from dataclasses import dataclass
from io import StringIO

from dataclass_wizard import JSONWizard


json_data = StringIO("""
{ "name": "foo",
  "some_object": { "field1": "bar", "field2" : "0" },
  "some_list_of_objects": [
      { "field1": "bar1", "field2" : "1" },
      { "field1": "bar2", "field2" : "2" },
      { "field1": "bar3", "field2" : "3" }
  ]
}
""")


@dataclass
class A(JSONWizard):
    name: str
    some_object: B
    some_list_of_objects: list[C]


@dataclass
class B:
    field1: str
    field2: int


@dataclass
class C:
    field1: str
    field2: int


a = A.from_json(json_data.read())

print(f'{a!r}')  # alternatively: print(repr(a))

Output

A(name='foo', some_object=B(field1='bar', field2=0), some_list_of_objects=[C(field1='bar1', field2=1), C(field1='bar2', field2=2), C(field1='bar3', field2=3)])

Loading from a JSON file

As per the suggestions in this post, I would discourage overriding the constructor method to pass the name of a JSON file to load the data from. Instead, I would suggest creating a helper class method as below, that can be invoked like A.from_json_file('file.json') if desired.

    @classmethod
    def from_json_file(cls, file_name: str):
        """Deserialize json file contents into an A object."""
        with open(file_name, 'r') as json_file:
            return cls.from_dict(json.load(json_file))

Suggestions

Note that variable annotations (or annotations in general) are subscripted using square brackets [] rather than parentheses as appears in the original version above.

some_list_of_objects: list(C)

In the above solution, I've instead changed that to:

some_list_of_objects: list[C]

This works because using subscripted values in standard collections was introduced in PEP 585. However, using the from __future__ import annotations statement introduced to Python 3.7 effectively converts all annotations to forward-declared string values, so that new-style annotations that normally only would work in Python 3.10, can also be ported over to Python 3.7 as well.

One other change I made, was in regards to swapping out the order of declared class annotations. For example, note the below:

class B:
  field1: int
  field2: str

However, note the corresponding field in the JSON data, that would be deserialized to a B object:

'some_object': { 'field1': 'bar', 'field2' : '0' },

In the above implementation, I've swapped out the field annotations in such cases, so class B for instance is declared as:

class B:
    field1: str
    field2: int
  • Related