Simple and flexible conversions between dataclasses and jsonable dictionaries.
Project description
dataclass-jsonable
Simple and flexible conversions between dataclasses and jsonable dictionaries.
It maps dataclasses to jsonable dictionaries but not json strings.
Features
- Easy to use.
- Supports common type annotations.
- Supports recursive conversions.
- Supports field-level and dataclass-level overriding.
Installation
Requirements: Python >= 3.7
Install via pip
:
pip install dataclass-jsonable
Quick Example
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from enum import IntEnum
from typing import List
from dataclass_jsonable import J
class Color(IntEnum):
BLACK = 0
BLUE = 1
RED = 2
@dataclass
class Pen(J):
color: Color
price: Decimal
produced_at: datetime
@dataclass
class Box(J):
pens: List[Pen]
box = Box(pens=[Pen(color=Color.BLUE, price=Decimal("20.1"), produced_at=datetime.now())])
# Encode to a jsonable dictionary.
d = box.json()
print(d) # {'pens': [{'color': 1, 'price': '20.1', 'produced_at': 1660023062}]}
# Construct dataclass from a jsonable dictionary.
print(Box.from_json(d))
APIs are only the two: .json()
and .from_json()
.
Built-in Supported Types
-
bool
,int
,float
,str
,None
encoded as it is.@dataclass class Obj(J): a: int b: str c: bool d: None Obj(a=1, b="b", c=True, d=None).json() # => {'a': 1, 'b': 'b', 'c': True, 'd': None}
-
Decimal
encoded tostr
.@dataclass class Obj(J): a: Decimal Obj(a=Decimal("3.1")).json() # => {'a': '3.1'}
-
datetime
encoded to timestamp integer via.timestamp()
method.timedelta
encoded to integer via.total_seconds()
method.@dataclass class Obj(J): a: datetime b: timedelta Obj(a=datetime.now(), b=timedelta(minutes=1)).json() # => {'a': 1660062019, 'b': 60}
-
Enum
andIntEnum
encoded to their values via.value
attribute.@dataclass class Obj(J): status: Status Obj(status=Status.DONE).json() # => {'status': 1}
-
Any
is encoded according to itstype
.@dataclass class Obj(J): a: Any Obj(1).json() # {'a': 1} Obj("a").json() # {'a': 'a'} Obj.from_json({"a": 1}) # Obj(a=1)
-
Optional[X]
is supported, butUnion[X, Y, ...]
is not.@dataclass class Obj(J): a: Optional[int] = None Obj(a=1).json() # => {'a': 1}
-
List[X]
,Tuple[X]
,Set[X]
are all encoded tolist
.@dataclass class Obj(J): a: List[int] b: Set[int] c: Tuple[int, str] d: Tuple[int, ...] Obj(a=[1], b={2, 3}, c=(4, "5"), d=(7, 8, 9)).json()) # => {'a': [1], 'b': [2, 3], 'c': [4, '5'], 'd': [7, 8, 9]} Obj.from_json({"a": [1], "b": [2, 3], "c": [4, "5"], "d": [7, 8, 9]})) # => Obj(a=[1], b={2, 3}, c=(4, '5'), d=(7, 8, 9))
-
Dict[str, X]
encoded todict
.@dataclass class Obj(J): a: Dict[str, int] Obj(a={"x": 1}).json() # => {'a': {'x': 1}} Obj.from_json({"a": {"x": 1}}) # => Obj(a={'x': 1})
-
Nested or recursively
JSONAble
(orJ
) dataclasses.@dataclass class Elem(J): k: str @dataclass class Obj(J): a: List[Elem] Obj([Elem("v")]).json() # => {'a': [{'k': 'v'}]} Obj.from_json({"a": [{"k": "v"}]}) # Obj(a=[Elem(k='v')])
-
Postponed annotations (the
ForwardRef
in PEP 563).@dataclass class Node(J): name: str left: Optional["Node"] = None right: Optional["Node"] = None root = Node("root", left=Node("left"), right=Node("right")) root.json() # {'name': 'root', 'left': {'name': 'left', 'left': None, 'right': None}, 'right': {'name': 'right', 'left': None, 'right': None}}
If these built-in default conversion behaviors do not meet your needs, or your type is not on the list, you can use json_options introduced below to customize it.
Customization / Overriding Examples
We can override the default conversion behaviors with json_options
,
which uses the dataclass field's metadata for field-level customization purpose,
and the namespace is j
.
The following pseudo code gives the pattern:
from dataclasses import field
from dataclass_jsonable import json_options
@dataclass
class Struct(J):
attr: T = field(metadata={"j": json_options(**kwds)})
An example list about json_options
:
-
Specific a custom dictionary key over the default field's name:
@dataclass class Person(J): attr: str = field(metadata={"j": json_options(name="new_attr")}) Person(attr="value").json() # => {"new_attr": "value"}
And more, we can use a function to specific a custom dictionary key. This may be convenient to work with class-level
__default_json_options__
attribute (check it below).@dataclass class Obj(J): simple_value: int = field(metadata={"j": json_options(name_converter=to_camel_case)}) Obj(simple_value=1).json() # => {"simpleValue": 1}
And we may specific a custom field name converter when converts dictionary to dataclass:
@dataclass def Person(J): name: str = field( metadata={ "j": json_options( name_converter=lambda x: x.capitalize(), name_inverter=lambda x: "nickname", ) } )
As the
Person
defined above, it will convert to dictionary like{"Name": "Jack"}
and can be loaded from{"nickname": "Jack"}
. -
Omit a field if its value is empty:
@dataclass class Book(J): name: str = field(metadata={"j": json_options(omitempty=True)}) Book(name="").json() # => {}
Further, we can specify what is 'empty' via option
omitempty_tester
:@dataclass class Book(J): attr: Optional[str] = field( default=None, metadata={ # By default, we test `empty` using `not x`. "j": json_options(omitempty=True, omitempty_tester=lambda x: x is None) }, ) Book(attr="").json() # => {'attr': ''} Book(attr=None).json() # => {}
-
Always skip a field. So we can stop some "private" fields from exporting:
@dataclass class Obj(J): attr: str = field(metadata={"j": json_options(skip=True)}) Obj(attr="private").json() # => {}
-
dataclasses's
field
allows us to pass adefault
ordefault_factory
argument to set a default value:@dataclass class Obj(J): attr: List[str] = field(default_factory=list, metadata={"j": json_options(**kwds)})
There's also an option
default_before_decoding
in dataclass-jsonable, which specifics a default value before decoding if the key is missing in the dictionary. Sometimes this way is more concise:@dataclass class Obj(J): updated_at: datetime = field(metadata={"j": json_options(default_before_decoding=0)}) Obj.from_json({}) # => Obj(updated_at=datetime.datetime(1970, 1, 1, 8, 0))
dataclass-jsonable also introduces a class-level similar option
__default_factory__
. If a field has nodefault
ordefault_factory
declared, and has nodefault_before_decoding
option used, this function will generate a default value according to its type, to prevent a "missing positional arguments" TypeError from rasing.from dataclass_jsonable import J, zero @dataclass class Obj(J): __default_factory__ = zero n: int s: str k: List[str] Obj.from_json({}) # => Obj(n=0, s='', k=[])
-
Override the default encoders and decoders.
This way, you have complete control over how to encode and decode at field level.
@dataclass class Obj(J): elems: List[str] = field( metadata={ "j": json_options( encoder=lambda x: ",".join(x), decoder=lambda x: x.split(","), ) } ) Obj(elems=["a", "b", "c"]).json() # => {'elems': 'a,b,c'} Obj.from_json({"elems": "a,b,c"}) # => Obj(elems=['a', 'b', 'c'])
The following code snippet about
datetime
is a very common example, you might want ISO format datetime conversion over timestamp integers.@dataclass class Record(J): created_at: datetime = field( default_factory=datetime.now, metadata={ "j": json_options( encoder=datetime.isoformat, decoder=datetime.fromisoformat, ) }, ) Record().json() # => {'created_at': '2022-08-09T23:23:02.543007'}
-
For some very narrow scenarios, we may need to execute a hook function before decoding, for example, the data to be decoded is a serialized json string, and but we still want to use the built-in decoder functions instead of making a new decoder.
import json @dataclass class Obj(J): data: Dict[str, Any] = field(metadata={"j": json_options(before_decoder=json.loads)}) Obj.from_json({"data": '{"k": "v"}'}) # => Obj(data={'k': 'v'})
-
Customize default behaviors at the class level.
If an option is not explicitly set at the field level, the
__default_json_options__
provided at the class level will be attempted.@dataclass class Obj(J): __default_json_options__ = json_options(omitempty=True) a: Optional[int] = None b: Optional[str] = None Obj(b="b").json() # => {'b': 'b'}
@dataclass class Obj(J): __default_json_options__ = json_options(name_converter=to_camel_case) status_code: int simple_value: str Obj2(status_code=1, simple_value="simple").json() # => {"statusCode": 1, "simpleValue": "simple"}
Debuging
It provides a method obj._get_origin_json()
,
it returns the original json dictionary which constructs instance obj
via from_json()
.
d = {"a": 1}
obj = Obj.from_json(d)
obj._get_origin_json()
# => {"a": 1}
License
BSD.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file dataclass-jsonable-0.1.0.tar.gz
.
File metadata
- Download URL: dataclass-jsonable-0.1.0.tar.gz
- Upload date:
- Size: 10.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.11.1
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | efeab81954372ff385c80898ed6b1d94eaf712fff9588a7eb732f3dc6eebebb6 |
|
MD5 | 2d8d933fb567eab5da2c83568aa8d0c8 |
|
BLAKE2b-256 | 980c8720ab99927ac19599ac8a51ee1c6935131f190c35ba1b8a026f4c0018b1 |