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