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?
Advertisement
Answer
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
