Fastclasses JSON
Inspired by Dataclasses JSON. This library attempts provide some basic functionality for encoding and decoding dataclasses with close to hand-written performance characteristics for large datasets.
from dataclasses import dataclass
from fastclasses_json import dataclass_json
@dataclass_json
@dataclass
class SimpleExample:
str_field: str
SimpleExample.from_dict({'str_field': 'howdy!'})
SimpleExample.from_json('{"str_field": "howdy!"}')
# SimpleExample(str_field='howdy!')
SimpleExample('hi!').to_dict()
# {'str_field': 'hi!'}
SimpleExample('hi!').to_json()
# '{"str_field":"hi!"}'
Installation
$ pip install fastclasses-json
Supported Types
-
typing.List[T]
whereT
is also decorated with@dataclass_json
typing.Optional[T]
typing.Optional[typing.List[T]]
typing.List[typing.Optional[T]]
-
typing.List[typing.List[typing.List[T]]]
etc typing.Dict[str, T]
-
enum.Enum
subclasses -
datetime.date
anddatetime.datetime
as ISO8601 format strings- NB: if
python-dateutil
is installed, it will be used instead of the standard library for parsing
- NB: if
-
decimal.Decimal
as strings -
uuid.UUID
as strings - Mutually recursive dataclasses.
any other types will just be left as is
from __future__ import annotations
from typing import Optional, List
@dataclass_json
@dataclass
class Russian:
doll: Optional[Doll]
@dataclass_json
@dataclass
class Doll:
russian: Optional[Russian]
Russian.from_dict({'doll': {'russian': {'doll': None}}})
# Russian(doll=Doll(russian=Russian(doll=None)))
Russian(Doll(Russian(None))).to_dict()
# {'doll': {'russian': {}}}
from enum import Enum
class Mood(Enum):
HAPPY = 'json'
SAD = 'xml'
@dataclass_json
@dataclass
class ILikeEnums:
maybe_moods: Optional[List[Mood]]
ILikeEnums.from_dict({}) # ILikeEnums(maybe_moods=None)
ILikeEnums.from_dict({'maybe_moods': ['json']}) # ILikeEnums(maybe_moods=[Mood.HAPPY])
ILikeEnums(maybe_moods=[Mood.HAPPY]).to_dict() # {'maybe_moods': ['json']}
from datetime import date
@dataclass_json
@dataclass
class Enitnelav:
romantic: date
Enitnelav.from_dict({'romantic': '2021-06-17'}) # Enitnelav(romantic=datetime.date(2021, 6, 17))
Enitnelav(romantic=date(2021, 6, 17)).to_dict() # {'romantic': '2021-06-17'}
from decimal import Decimal
from uuid import UUID
@dataclass_json
@dataclass
class TaxReturn:
number: UUID
to_pay: Decimal # 😱
TaxReturn.from_dict({'number': 'e10be89e-938f-4b49-b4cf-9765f2f15298', 'to_pay': '0.01'})
# TaxReturn(number=UUID('e10be89e-938f-4b49-b4cf-9765f2f15298'), to_pay=Decimal('0.01'))
TaxReturn(UUID('e10be89e-938f-4b49-b4cf-9765f2f15298'), Decimal('0.01')).to_dict()
# {'number': 'e10be89e-938f-4b49-b4cf-9765f2f15298', 'to_pay': '0.01'}
we are not a drop-in replacement for Dataclasses JSON. There are plenty of cases to use this in spite.
Configuration
Per-field configuration is done by including a "fastclasses_json"
dict
in the field metadata dict.
-
encoder
: a function to convert a given field value when converting from adataclass
to adict
or to JSON. Can be any callable. -
decoder
: a function to convert a given field value when converting from JSON or a dict into the pythondataclass
. Can be any callable. -
field_name
: the name the field should be called in the JSON output.
example
@dataclass_json
@dataclass
class Coach:
from_: str = field(metadata={
"fastclasses_json": {
"field_name": "from",
"encoder": lambda v: v[:5].upper(),
}
})
to_: str = field(metadata={
"fastclasses_json": {
"field_name": "to",
"encoder": lambda v: v[:5].upper(),
}
})
Coach("London Victoria", "Amsterdam Sloterdijk").to_dict()
# {'from': 'LONDO', 'to': 'AMSTE'}
Whole tree configuration options
How to use other field naming conventions
The field_name_transform
option allows tranforming field names of all
dataclasses that are serialized / deserialized.
from __future__ import annotations
from fastclasses_json import dataclass_json
from dataclasses import dataclass
@dataclass_json(field_name_transform=str.upper)
@dataclass
class Box:
dimensions: Dimensions
weight_in_g: int
@dataclass
class Dimensions:
height_in_mm: int
width_in_mm: int
depth_in_mm: int
Box(Dimensions(12, 24, 35), 944).to_dict()
# {'DIMENSIONS': {'HEIGHT_IN_MM': 12, 'WIDTH_IN_MM': 24, 'DEPTH_IN_MM': 35}, 'WEIGHT_IN_G': 944}
Type checking (i.e. using mypy)
If using type annotations in your code, you may notice type errors when type
checking classes that use the @dataclass_json
decorator.
% mypy tests/for_type_checking.py
tests/for_type_checking.py:27: error: "A" has no attribute "to_json"
tests/for_type_checking.py:28: error: "Type[A]" has no attribute "from_dict"
There are two techniques for overcoming this, one which is simpler but likely to break or be unstable between versions of python and mypy; and one which is a bit more work on your part.
Mypy plugin
Changes in python and mypy are likely to lead to a game of cat and mouse, but
for the moment, we have a plugin that you can configure in your setup.cfg
% cat setup.cfg
[mypy]
plugins = fastclasses_json.mypy_plugin
Mixin with stub methods
There is a mixin containing stub methods for converting to and from dicts and JSON. This can be useful if the mypy plugin breaks or if you are using a different type checker.
from dataclasses import dataclass
from fastclasses_json import dataclass_json, JSONMixin
@dataclass_json
@dataclass
class SimpleTypedExample(JSONMixin):
what_a_lot_of_hassle_these_types_eh: str
print(SimpleTypedExample.from_dict({'what_a_lot_of_hassle_these_types_eh': 'yes'}))
% mypy that_listing_above.py
Success: no issues found in 1 source file
Notice that you have to use both the @dataclass_json
decorator and the
JSONMixin
mixin. How very annoying!
Migration & Caveats
None
Fields with the value None
are not included in the produced JSON. This helps
keep the JSON nice and compact
from dataclasses import dataclass
from fastclasses_json import dataclass_json
from typing import Optional
@dataclass_json
@dataclass
class Farm:
sheep: Optional[int]
cows: Optional[int]
Farm(sheep=None, cows=1).to_json()
# '{"cows":1}'
infer_missing
Fastclasses JSON does not get annoyed if fields are missing when deserializing.
Missing fields are initialized as None
. This differs from the defaults in
Dataclasses JSON.
from dataclasses import dataclass
from fastclasses_json import dataclass_json
@dataclass_json
@dataclass
class Cupboard:
num_hats: int
num_coats: int
Cupboard.from_dict({'num_hats': 2})
# Cupboard(num_hats=2, num_coats=None)
In Dataclasses JSON, there is the infer_missing
parameter that gives this behaviour.
To make migration easier, from_dict
and from_json
takes the dummy parameter
infer_missing
, so that the following code works the same and does
not cause errors:
Cupboard.from_dict({'num_hats': 2}, infer_missing=True)
# Cupboard(num_hats=2, num_coats=None)
letter_case
Fastclasses JSON does not have letter_case
, instead see
field_name_transform
under Configuration
which can achieve the same goals.