Skip to content

JSON conversion

The JSONDataclass mixin provides automatic conversion to and from JSON.

Usage Example

Define a JSONDataclass.

from dataclasses import dataclass
from typing import Optional

from fancy_dataclass import JSONDataclass


@dataclass
class Person(JSONDataclass):
    name: str
    age: int
    height: float
    hobbies: list[str]
    awards: Optional[list[str]] = None

Convert to/from a Python dict.

>>> person = Person(
    name='John Doe',
    age=47,
    height=71.5,
    hobbies=['reading', 'juggling', 'cycling']
)

# default values are suppressed by default
>>> person.to_dict()
{'name': 'John Doe',
 'age': 47,
 'height': 71.5,
 'hobbies': ['reading', 'juggling', 'cycling']}

# include all the values
>>> person.to_dict(full=True)
{'name': 'John Doe',
 'age': 47,
 'height': 71.5,
 'hobbies': ['reading', 'juggling', 'cycling'],
 'awards': None}

>>> new_person = Person.from_dict(person.to_dict())
>>> new_person == person
True

Convert to/from a JSON string.

>>> person = Person(
    name='John Doe',
    age=47,
    height=71.5,
    hobbies=['reading', 'juggling', 'cycling']
)

>>> json_string = person.to_json_string(indent=2)

>>> print(json_string)
{
  "name": "John Doe",
  "age": 47,
  "height": 71.5,
  "hobbies": [
    "reading",
    "juggling",
    "cycling"
  ]
}

>>> new_person = Person.from_json_string(json_string)
>>> person == new_person
True

Details

JSONDataclass inherits from DictDataclass, which can be used to convert dataclasses to/from Python dicts via to_dict and from_dict. You may use DictDataclass if you do not need to interact with JSON serialized data.

Class and Field Settings

You may customize the behavior of a JSONDataclass subclass by passing keyword arguments upon inheritance (see mixin class settings). See DictDataclassSettings for the full list of settings. For field-specific settings, see DictDataclassFieldSettings.

Suppressing Defaults

One setting, suppress_defaults, is set to True by default. This will suppress fields in an output dict or JSON whose values match the class's default value. While this is often helpful to keep the output smaller in size, it is sometimes better to be explicit. To override this behavior, you can set suppress_defaults=False.

@dataclass
class A(JSONDataclass):
    x: int = 5

@dataclass
class B(JSONDataclass, suppress_defaults=False):
    x: int = 5

print(A().to_json_string())
{}

print(B().to_json_string())
{"x": 5}

Suppressing Null Values

You can suppress fields with null values by supplying suppress_none=True:

@dataclass
class C(JSONDataclass, suppress_none=True):
    x: Optional[int]

print(C(1).to_json_string()
{"x": 1}

print(C(None).to_json_string()
{}

You can be more fine-grained about handling the output behavior of specific fields by setting flags in their field settings:

  • Setting suppress_default to False or True will override the class setting at the field level.
  • Setting suppress_none to False or True will override the class setting at the field level.
  • Setting suppress to False or True will force inclusion or exclusion of the field, regardless of suppress_defaults or suppress_none settings.

Including Types in Output

Another setting of note is store_type. This relates to type inference when loading an object from a dict or JSON blob. Suppose you have a class like:

@dataclass
class Circle(JSONDataclass):
    radius: float

Converting a Circle object to a JSON string:

print(Circle(3).to_json_string())
{"radius": 3}

This may be undesirable, since the output does not make it clear what type of thing it is. To include the type in the output, you may set store_type='name':

@dataclass
class Circle(JSONDataclass, store_type='name'):
    radius: float

print(Circle(3).to_json_string())
{"type": "Circle", "radius": 3}

Sometimes it is better to store the fully qualified type name instead. You can do this by setting store_type to 'qualname':

@dataclass
class Circle(JSONDataclass, store_type='qualname'):
    radius: float

print(Circle(3).to_json_string())
{"type": "my_module.Circle", "radius": 3}

(Here, my_module is the name of the module in which Circle is defined.)

Setting store_type='qualname' is particularly useful when dealing with inheritance hierarchies. For example, if you try:

@dataclass
class Shape(JSONDataclass):
    ...

@dataclass
class Circle(Shape):
    radius: float

This will raise the following error: TypeError: when subclassing a JSONDataclass, you must set store_type to a value other than 'auto', or subclass JSONBaseDataclass instead. This is saying that when you subclass JSONDataclass, you must explicitly override store_type (preferably with 'qualname', to prevent type ambiguity when converting back from JSON). An alternative to setting store_type is subclassing JSONBaseDataclass instead of JSONDataclass. This will set store_type to 'qualname' automatically.

Let's see why this is useful:

@dataclass
class Shape(JSONBaseDataclass):
    ...

@dataclass
class Circle(Shape):
    radius: float

@dataclass
class Rectangle(Shape):
    length: float
    width: float

Now you can use the base class, Shape, to convert from different subtypes:

shape_dicts = [{"type": "Circle", "radius": 3}, {"type": "Rectangle", "length": 3, "width": 5}]
shapes = [Shape.from_dict(d) for d in shape_dicts]

print(shapes)
[Circle(radius=3.0), Rectangle(length=3.0, width=5.0)]

Additional Customization

To customize the JSON output format, you may pass keyword arguments to to_json or to_json_string; these will get passed along to json.dump. For example, ensure_ascii=False will allow non-ASCII output, and indent=4 will indent the JSON with 4 spaces.

Note

JSONDataclass is configured to use the default JSON settings provided by Python's standard json library. This allows out-of-range float values like nan and inf to be represented as NaN and Infinity, which are not strictly part of the JSON standard. To disallow these values, you can pass allow_nan=False when calling to_json or to_json_string, which will raise a ValueError if such values occur.

To customize JSON encoding itself, a subclass of JSONDataclass may override the json_encoder method. This should return a json.JSONEncoder subclass.

You can also customize how JSON keys are decoded. For example, you may want to translate an integer key in a JSON file like "1" to the integer 1. To accomplish this, override the json_key_decoder method.