Package: dataclass-jsonable
dataclass-jsonable中文说明Simple and flexible conversions between dataclasses and jsonable dictionaries.It maps dataclasses to jsonable dictionaries but not json strings.FeaturesEasy to use.Supports common type annotations.Supports recursive conversions.Supports field-level and dataclass-level overriding.InstallationRequirements: Python >= 3.7Install viapip:pip install dataclass-jsonableQuick ExamplefromdataclassesimportdataclassfromdatetimeimportdatetimefromdecimalimportDecimalfromenumimportIntEnumfromtypingimportListfromdataclass_jsonableimportJclassColor(IntEnum):BLACK=0BLUE=1RED=2@dataclassclassPen(J):color:Colorprice:Decimalproduced_at:datetime@dataclassclassBox(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 Typesbool,int,float,str,Noneencoded as it is.@dataclassclassObj(J):a:intb:strc:boold:NoneObj(a=1,b="b",c=True,d=None).json()# => {'a': 1, 'b': 'b', 'c': True, 'd': None}Decimalencoded tostr.@dataclassclassObj(J):a:DecimalObj(a=Decimal("3.1")).json()# => {'a': '3.1'}datetimeencoded to timestamp integer via.timestamp()method.timedeltaencoded to integer via.total_seconds()method.@dataclassclassObj(J):a:datetimeb:timedeltaObj(a=datetime.now(),b=timedelta(minutes=1)).json()# => {'a': 1660062019, 'b': 60}EnumandIntEnumencoded to their values via.valueattribute.@dataclassclassObj(J):status:StatusObj(status=Status.DONE).json()# => {'status': 1}Anyis encoded according to itstype.@dataclassclassObj(J):a:AnyObj(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.@dataclassclassObj(J):a:Optional[int]=NoneObj(a=1).json()# => {'a': 1}List[X],Tuple[X],Set[X]are all encoded tolist.@dataclassclassObj(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.@dataclassclassObj(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 recursivelyJSONAble(orJ) dataclasses.@dataclassclassElem(J):k:str@dataclassclassObj(J):a:List[Elem]Obj([Elem("v")]).json()# => {'a': [{'k': 'v'}]}Obj.from_json({"a":[{"k":"v"}]})# Obj(a=[Elem(k='v')])Postponed annotations (theForwardRefinPEP 563).@dataclassclassNode(J):name:strleft:Optional["Node"]=Noneright:Optional["Node"]=Noneroot=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 usejson_optionsintroduced below to customize it.Customization / Overriding ExamplesWe can override the default conversion behaviors withjson_options,
which uses the dataclass field's metadata for field-level customization purpose,
and the namespace isj.The following pseudo code gives the pattern:fromdataclassesimportfieldfromdataclass_jsonableimportjson_options@dataclassclassStruct(J):attr:T=field(metadata={"j":json_options(**kwds)})An example list aboutjson_options:Specific a custom dictionary key over the default field's name:@dataclassclassPerson(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).@dataclassclassObj(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:@dataclassdefPerson(J):name:str=field(metadata={"j":json_options(name_converter=lambdax:x.capitalize(),name_inverter=lambdax:"nickname",)})As thePersondefined above, it will convert to dictionary like{"Name": "Jack"}and can be loaded from{"nickname": "Jack"}.Omit a field if its value is empty:@dataclassclassBook(J):name:str=field(metadata={"j":json_options(omitempty=True)})Book(name="").json()# => {}Further, we can specify what is 'empty' via optionomitempty_tester:@dataclassclassBook(J):attr:Optional[str]=field(default=None,metadata={# By default, we test `empty` using `not x`."j":json_options(omitempty=True,omitempty_tester=lambdax:xisNone)},)Book(attr="").json()# => {'attr': ''}Book(attr=None).json()# => {}Always skip a field. So we can stop some "private" fields from exporting:@dataclassclassObj(J):attr:str=field(metadata={"j":json_options(skip=True)})Obj(attr="private").json()# => {}Always keep a field without encoding nor decoding, this prevents the default encoding/decoding behavior:@dataclassclassObj(J):timestamp:datetime=field(metadata={"j":json_options(keep=True)})Obj(timestamp=datetime.now()).json()# =>  {'timestamp': datetime.datetime(2023, 9, 5, 14, 54, 24, 679103)}dataclasses'sfieldallows us to pass adefaultordefault_factoryargument to
set a default value:@dataclassclassObj(J):attr:List[str]=field(default_factory=list,metadata={"j":json_options(**kwds)})There's also an optiondefault_before_decodingin dataclass-jsonable,
which specifics a default value before decoding if the key is missing in the dictionary.
Sometimes this way is more concise:@dataclassclassObj(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 nodefaultordefault_factorydeclared, and has nodefault_before_decodingoption used,
this function will generate a default value according to its type, to prevent a
"missing positional arguments" TypeError from raising.fromdataclass_jsonableimportJ,zero@dataclassclassObj(J):__default_factory__=zeron:ints:strk: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.@dataclassclassObj(J):elems:List[str]=field(metadata={"j":json_options(encoder=lambdax:",".join(x),decoder=lambdax: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 aboutdatetimeis a very common example,
you might want ISO format datetime conversion over timestamp integers.@dataclassclassRecord(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'}dataclass-jsonable givesencoderanddecoderbetter alias names since 0.1.1:to_jsonandfrom_json.@dataclassclassObj(J):elems:List[str]=field(metadata={"j":json_options(to_json=lambdax:",".join(x),# Alias for encoderfrom_json=lambdax:x.split(","),# Alias for decoder)})Obj(elems=["a","b","c"]).json()# => {'elems': 'a,b,c'}Obj.from_json({"elems":"a,b,c"})# => Obj(elems=['a', 'b', 'c'])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.importjson@dataclassclassObj(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.@dataclassclassObj(J):__default_json_options__=json_options(omitempty=True)a:Optional[int]=Noneb:Optional[str]=NoneObj(b="b").json()# => {'b': 'b'}@dataclassclassObj(J):__default_json_options__=json_options(name_converter=to_camel_case)status_code:intsimple_value:strObj2(status_code=1,simple_value="simple").json()# => {"statusCode": 1, "simpleValue": "simple"}DebugingIt provides a methodobj._get_origin_json(),
it returns the original json dictionary which constructs instanceobjviafrom_json().d={"a":1}obj=Obj.from_json(d)obj._get_origin_json()# => {"a": 1}LicenseBSD.