Package: typing-json
Welcome to thetyping_jsonlibraryThetyping_jsonlibrary offers type-aware JSON encoding and decoding functionalities, on top of those offered by the builtinjsonlibrary. The functionsdump,dumps,loadandloadsmirror the functionality of theirjsoncounterparts, adding type-aware encoding/decoding and runtime type-checking of decoded objects.Supported types include JSON basic types,Decimal, typed collections from thetypinglibrary, literal types, union types, optional types and typed namedtuples. For a full list of types, see below.
The functionis_instanceextends the functionality of the builtinisinstanceto include all the additional types supported by this library.Thedocumentationfor this library was generated withpdoc.You can install thetyping_jsonlibrary with pip:pip install typing_jsonMain goalsThere are two main drivers behind the development of thetyping_jsonlibrary:Type-awareserialisationof data using JSON.Runtimevalidationof JSON data for use with static typing.The first goal of thetyping_jsonlibrary is to automate the serialisation of statically typed data in Python.
In a statically typed Python application (e.g. one validated usingmypy), data is often structured using simple static types.
Thetyping_jsonlibrary uses these types to automate the process of JSON serialisation and de-serialisation, ensuring that the serialised data can subsequently be de-serialised into a valid instance of the original static type, equivalent to the instance that was originally serialised.The second goal of thetyping_jsonlibrary is to automate the validation of JSON data against existing static types.
When JSON data is loaded dynamically into a statically typed Python application, it needs to be validated to ensure that it conforms to whatever static types are being used as its specification.
Thetyping_jsonlibrary uses these types to automate the validation process, i.e. to perform runtime type-checking of the JSON data against the static types.
This guarantees that data successfully de-serialised from JSON using theload/loadsfunctions of thetyping_jsonlibrary conforms to the static type provided.Types supportedThe following types are currently supported by thetyping_jsonlibrary:the JSON basic typesbool,int,float,strandNoneType(technicallytype(None), butNonecan be used as an alias);the typeDecimalfrom thedecimalbuiltin (cf. below for the handling of numerical types);the following typed collections from thetypingbuiltin library, as long as all generic type arguments are themselves supported:List,Tuple,Deque,Set,FrozenSet;typed namedtuples constructed usingNamedTuplefrom the builtintypinglibrary, as long as all fields are of supported type;typed dictionaries constructed usingTypedDictfrom thetyping_extensionslibrary, as long as all fields are of supported type;the following typed collections from thetypingbuiltin library, as long as the generic key/value generic type arguments are themselves supported:Dict,MappingandOrderedDict(see below for additional requirements on the key generic type arguments and special behaviour on JSON encoding/decoding of keys);enumeration typestheLiteraltypes from thetyping_extensionslibrary, as long as all literal are of one of the JSON basic types above;OptionalandUniontypes from thetypingbuiltin library, as long as all generic type arguments are themselves supported (cf. below for a caveat aboutUniontypes).The following function can be used at runtime to check whethertis a type supported by thetyping_jsonlibrary:defis_json_encodable(t:Any,failure_callback=None)->bool:...The optional parameterfailure_callbackcan be used to pass aCallable[[str], None]that will be used to log any error messages.
The following provides an example of usage:# Python 3.7.4>>>fromtypingimportDict,List,Set,Tuple>>>fromtyping_jsonimportis_json_encodable>>>error_log=[]>>>my_callback=lambdas:error_log.append(s)>>>is_json_encodable(List[Dict[Set[int],int]],failure_callback=my_callback)False>>>error_log['Type typing.Set[int] is not keyable.','Type of keys in typing.Dict[typing.Set[int], int] is not keyable.','Type of elements in typing.List[typing.Dict[typing.Set[int], int]] is not json-encodable.']Overview of encoding/decoding functionsThere are three pairs of encoding/decoding functions offered by thetyping_jsonlibrary, for use in three different circumstances.The functionsto_json_obj/from_json_objoffer runtime conversion of instances of supported types to/from JSON objects.defto_json_obj(obj:Any,t:Type,use_decimal:bool=False)->Any:...deffrom_json_obj(obj:Any,t:Type,cast_decimal:bool=True)->Any:...The functionsdumps/loadsoffer (de-)serialisation of instances of supported types to/from JSON formatted strings.defdumps(obj:Any,encoded_type:Type,**kwargs)->str:...defloads(s:str,decoded_type:Type,cast_decimal:bool=True,**kwargs)->Any:...The functionsdump/loadoffer (de-)serialisation of instances of supported types to/from JSON formatted IO streams.defdump(obj:Any,encoded_type:Type,fp,**kwargs)->None:...defload(fp,decoded_type:Type,cast_decimal:bool=True,**kwargs)->Any:...The callsdump(obj, t, fp)/dumps(obj, t)first useto_json_obj(obj, t)to encode an instanceobjof a supported typetinto a JSON objectobj_json, then calljson.dump(obj_json)/json.dumps(obj_json)to serialiseobj_jsonto a file objectfpor string.Conversely, the callsload(fp, t)/loads(s, t)first calljson.load(fp)/json.loads(s)to deserialise a JSON objectobj_jsonfrom a file objectfpor strings, then callfrom_json_obj(obj_json, t)to decode an instanceobjof a supported typetfromobj_json.In all functions above,TypeErroris raised if the object passed does not match the type specified.
This runtime type-checking is performed by the functionis_instance:defis_instance(obj:Any,t:Type,failure_callback=None,cast_decimal:bool=True)->bool:...The functionis_instanceextends the behaviour of the builtinisinstanceto type-checking of instancesobjof all typestsupported by thetyping_jsonlibrary.
Most importantly, this includes the generic typed collections of thetypinglibrary, and features a slight alteration of behaviour on booleans and numerical types.Usingdump,dumps,loadandloadsThe functionsdump,dumps,loadandloadsin thetyping_jsonlibrary mirror their builtinjsoncounterparts, with a couple of exceptions:an additional parameterencoded_type(resp.decoded_type) is used indump/dumps(resp. inload/loads) to specify the type to be used in the JSON encoding (resp. decoding);an additional optional parametercast_decimal(default:True) is used inload/loadsto specify whether instances ofDecimal(used by default to parse float literals) should be silently cast tointandfloatwherever the type requires them to.Aside from the additional type parameter, the usage ofdump,dumps,loadandloadsis the same as that of theirjsoncounterparts:# Python 3.7.4>>>fromtypingimportDict>>>fromtyping_jsonimportload# myexpenses.json:## {#   "home": 150.25,#   "travel": 78.90,#   "entertainment": 52.00# }#>>>withopen("myexpenses.json","r")asfp:...load(fp,Dict[str,float])...{"home":150.25,"travel":78.90,"entertainment":52.00}# Python 3.7.4>>>fromtypingimportDict>>>fromtyping_jsonimportloads>>>s='{"home": 150.25, "travel": 78.9, "entertainment": 52.0}'>>>loads(s,Dict[str,float]){"home":150.25,"travel":78.90,"entertainment":52.00}# Python 3.7.4>>>fromtypingimportDict>>>fromtyping_jsonimportdump>>>myexpenses={"home":150.25,"travel":78.90,"entertainment":52.00}>>>withopen("myexpenses.json","w")asfp:...dump(myexpenses,Dict[str,float],fp)...# myexpenses.json:## {#   "home": 150.25,#   "travel": 78.90,#   "entertainment": 52.00# }#>>>fromtypingimportDict>>>fromtyping_jsonimportloads>>>myexpenses={"home":150.25,"travel":78.90,"entertainment":52.00}>>>dumps(myexpenses,Dict[str,float])'{"home": 150.25, "travel": 78.9, "entertainment": 52.0}'Basic typesOn JSON basic types, theto_json_objandfrom_json_objfunctions return their argument unchanged:# Python 3.7.4>>>fromtyping_jsonimportto_json_obj>>>to_json_obj(True,bool)True>>>to_json_obj(1,int)1>>>to_json_obj(1.5,float)1.5>>>to_json_obj("hello",str)"hello">>>to_json_obj(None,type(None))None>>>to_json_obj(None,None)# `None` is alias for `type(None)`NoneThe exact same outcomes above are obtained ifto_json_objis replaced withfrom_json_obj.The behaviour ofis_instanceon JSON basic types features two slight alterations from the behaviour of the builtinisinstance.
Firstly, theboolliteralsTrueandFalseare not deemed to be of typeintbyis_instance, but they are by the builtinisinstance:# Python 3.7.4>>>fromtyping_jsonimportis_instance>>>isinstance(False,int)# builtinTrue>>>isinstance(True,int)# builtinTrue>>>is_instance(False,int)# typing_jsonFalse>>>is_instance(True,int)# typing_jsonFalseSecondly, instances ofintare deemed to be of typefloatbyis_instance, but they are not by the builtinisinstance:# Python 3.7.4>>>fromtyping_jsonimportis_instance>>>isinstance(1,int)# builtinTrue>>>isinstance(1,float)# builtinFalse>>>is_instance(1,int)# typing_jsonTrue>>>is_instance(1,float)# typing_jsonTrueNumber typesWhen parsing JSON strings, from file object usingloador from string instances usingloads, the default behaviour is to use the constructor of classDecimalfrom the builtindecimallibrary to parse floating point literals.
This informs the following handling of number types in theto_json_obj/from_json_objfunctions.The default behaviour infrom_json_objis to silently decode instances ofDecimalto instances ofintandfloat, according to the type specified:# Python 3.7.4>>>fromdecimalimportDecimal>>>fromtyping_jsonimportfrom_json_obj>>>from_json_obj(Decimal("1.2"),Decimal)Decimal("1.2")>>>from_json_obj(Decimal("1.2"),float)1.2>>>from_json_obj(Decimal("1.0"),Decimal)Decimal("1.0")>>>from_json_obj(Decimal("1.0"),float)1.0>>>from_json_obj(Decimal("1.0"),int)1The optional parametercast_decimaloffrom_json_obj(default:True) can be set toFalseto disable the silent conversion ofDecimaltofloatandint:# Python 3.7.4>>>fromdecimalimportDecimal>>>fromtyping_jsonimportfrom_json_obj>>>from_json_obj(Decimal("1.2"),Decimal,cast_decimal=False)Decimal("1.2")>>>from_json_obj(Decimal("1.2"),float,cast_decimal=False)# TypeError: Object Decimal('1.2') is not of json basic type t=<class 'float'>.>>>from_json_obj(Decimal("1.0"),Decimal,cast_decimal=False)Decimal("1.0")>>>from_json_obj(Decimal("1.0"),float,cast_decimal=False)# TypeError: Object Decimal('1.0') is not of json basic type t=<class 'float'>.>>>from_json_obj(Decimal("1.0"),int,cast_decimal=False)# TypeError: Object Decimal('1.0') is not of json basic type t=<class 'int'>.To ensure that decimal precision is maintained, instancesDecimalare ordinarily encoded into JSON as strings:# Python 3.7.4>>>fromdecimalimportDecimal>>>fromtyping_jsonimportto_json_obj>>>to_json_obj(Decimal("1.2"),Decimal)"1.2">>>to_json_obj(Decimal("-16"),Decimal)"-16"The optional parameteruse_decimalofto_json_obj(default:True) can be set toTrueto instead allow instances ofDecimalto be used directly in JSON objects:# Python 3.7.4>>>fromdecimalimportDecimal>>>fromtyping_jsonimportto_json_obj>>>to_json_obj(Decimal("1.2"),Decimal,use_decimal=True)Decimal("1.2")>>>to_json_obj(Decimal("-16"),Decimal,use_decimal=True)Decimal("-16")Finally, integers are always silently converted to floating point numbers infrom_json_obj, but trying to convert floating point numbers to integers will always raise an error, regardless of whether the encoded number is an integer:# Python 3.7.4>>>fromdecimalimportDecimal>>>fromtyping_jsonimportfrom_json_obj>>>from_json_obj(1,int)1>>>from_json_obj(1,float)1.0>>>from_json_obj(1.0,float)1.0>>>from_json_obj(1.0,int)# TypeError: Object 1.0 is not of json basic type t=<class 'int'>.SequencesInstances ofList,TupleandDequeare encoded byto_json_objas JSON lists, with their elements recursively encoded:# Python 3.7.4>>>fromcollectionsimportdeque>>>fromdecimalimportDecimal>>>fromtypingimportDeque,List,Tuple>>>fromtyping_jsonimportto_json_obj>>>to_json_obj([1,2,3],List[int])[1,2,3]>>>to_json_obj((1,2.5,Decimal("3.5")),Tuple[int,float,Decimal])[1,2.5,"3.5"]>>>to_json_obj(deque(["a","b","c"]),Deque[str])["a","b","c"]>>>to_json_obj(((0,Decimal("0.5")),(1,Decimal("3"))),Tuple[Tuple[int,Decimal],...])[[0,"0.5"],[1,"3"]]JSON lists are are decoded byfrom_json_objto instances ofList,TupleandDequedepending on the specified type, with elements recursively decoded from the elements of the JSON list:# Python 3.7.4>>>fromcollectionsimportdeque>>>fromdecimalimportDecimal>>>fromtypingimportDeque,List,Tuple>>>fromtyping_jsonimportfrom_json_obj>>>from_json_obj([1,2,3],List[int])[1,2,3]>>>from_json_obj([1,2.5,'3.5'],Tuple[int,float,Decimal])(1,2.5,Decimal("3.5"))>>>from_json_obj(["a","b","c"],Deque[str])deque(["a","b","c"])>>>from_json_obj([[0,"0.5"],[1,"3"]],Tuple[Tuple[int,Decimal],...])((0,Decimal("0.5")),(1,Decimal("3")))SetsInstances ofSetandFrozenSetare encoded byto_json_objas JSON lists, with their elements recursively encoded:# Python 3.7.4>>>fromdecimalimportDecimal>>>fromtypingimportFrozenSet,Set>>>fromtyping_jsonimportto_json_obj>>>to_json_obj({1,2,3},Set[int])[1,2,3]>>>to_json_obj(frozenset({Decimal("1.5"),Decimal("2.5")}),FrozenSet[Decimal])["1.5","2.5"]JSON lists are are decoded byfrom_json_objto instances ofSetandFrozenSetdepending on the specified type, with elements recursively decoded from the elements of the JSON list:# Python 3.7.4>>>fromdecimalimportDecimal>>>fromtypingimportFrozenSet,Set>>>fromtyping_jsonimportfrom_json_obj>>>from_json_obj([1,2,3],Set[int]){1,2,3}>>>from_json_obj(["1.5","2.5"],FrozenSet[Decimal])frozenset({Decimal("1.5"),Decimal("2.5")})NamedTuplesInstances of typed namedtuples constructed withNamedTupleare encoded byto_json_objas JSON dictionaries (ordered), with the field names as their keys and the field values recursively encoded:# Python 3.7.4>>>fromcollectionsimportOrderedDict>>>fromtypingimportNamedTuple,Set,Tuple>>>fromtyping_jsonimportto_json_obj>>>classNetwork(NamedTuple):...nodes:Set[int]...edges:Set[Tuple[int,int]]...>>>network=Network({0,1,2},{(0,1),(1,2),(0,2)})>>>to_json_obj(network,Network)OrderedDict([('nodes',[0,1,2]),('edges',[[0,1],[0,2],[1,2]])])>>>dict(to_json_obj(network,Network)){'nodes':[0,1,2],'edges':[[0,1],[0,2],[1,2]]}JSON dictionaries are decoded byfrom_json_objto instances of typed namedtuples depending on the specified type, with fields values recursively decoded from the values of the dictionary:# Python 3.7.4>>>fromtypingimportNamedTuple,Set,Tuple>>>fromtyping_jsonimportfrom_json_obj>>>classNetwork(NamedTuple):...nodes:Set[int]...edges:Set[Tuple[int,int]]...>>>from_json_obj({'nodes':[0,1,2],'edges':[[0,1],[0,2],[1,2]]},Network)Network(nodes={0,1,2},edges={(0,1),(0,2),(1,2)})Whilecollections.OrderedDictis always used byto_json_objwhen encoding typed namedtuples, butfrom_json_objwill also accept ordinary dictionaries (because the order of fields is already determined by the namedtuple type).
If the namedtuple has fields with default values,from_json_objwill use the default value for any field not appearing in the dictionary:# Python 3.7.4>>>fromtypingimportNamedTuple,Set,Tuple>>>fromtyping_jsonimportfrom_json_obj>>>classEmployee(NamedTuple):...name:str...id:int=3...>>>from_json_obj({"name":"Gill","id":2},Employee)Employee(name='Gill',id=2)# "id" value from dictionary>>>from_json_obj({"name":"John"},Employee)Employee(name='John',id=3)# default "id" value from `Employee`>>>from_json_obj({"id":0},Employee)# TypeError: Object {'id': 0} does not have the required keys:# t=<class '__main__.Employee'>, missing keys {'name'}.If a field in the namedtuple does not have a default value and does not appear in the dictionary,from_json_objwill raiseTypeError.Typed DictionariesInstances of typed dictionaries constructed withTypedDictare encoded byto_json_objas JSON dictionaries (ordered), with the same keys and JSON-encoded values:# Python 3.8.3>>>fromcollectionsimportOrderedDict>>>fromtypingimportSet,Tuple>>>fromtyping_extensionsimportTypedDict>>>fromtyping_jsonimportto_json_obj>>>classNetwork(TypedDict):...nodes:Set[int]...edges:Set[Tuple[int,int]]...>>>network={"nodes":{0,1,2},"edges":{(0,1),(1,2),(0,2)}}>>>to_json_obj(network,Network)OrderedDict([('nodes',[0,1,2]),('edges',[[0,1],[0,2],[1,2]])])>>>dict(to_json_obj(network,Network)){'nodes':[0,1,2],'edges':[[0,1],[0,2],[1,2]]}JSON dictionaries are decoded byfrom_json_objto instances of typed dictionaries depending on the specified type, with values recursively decoded from the values of the dictionary:# Python 3.8.3>>>fromtypingimportSet,Tuple>>>fromtyping_extensionsimportTypedDict>>>fromtyping_jsonimportfrom_json_obj>>>classNetwork(TypedDict):...nodes:Set[int]...edges:Set[Tuple[int,int]]...>>>from_json_obj({'nodes':[0,1,2],'edges':[[0,1],[0,2],[1,2]]},Network){"nodes":{0,1,2},"edges":{(0,1),(1,2),(0,2)}}Whilecollections.OrderedDictis always used byto_json_objwhen encoding typed dictionaries, butfrom_json_objwill also accept ordinary dictionaries (because the order of fields is already determined by the typed dictionary).DictionariesInstances ofMappingandDictare encoded byto_json_objas JSON dictionaries, with their keys and values recursively encoded and their keys stringified if necessary (cf. below).
Instances ofOrderedDictfollow the exact same rules, but are encoded as instances ofcollections.OrderedDictrather than instances ofdict.# Python 3.7.4>>>importcollections>>>fromdecimalimportDecimal>>>importtyping>>>fromtypingimportDict,Mapping,Tuple>>>fromtyping_jsonimportto_json_obj>>>vect={"x":(Decimal("1.0"),Decimal("0.0")),"y":(Decimal("0.0"),Decimal("1.0"))}>>>to_json_obj(vect,Dict[str,Tuple[Decimal,Decimal]]){"x":["1.0","0.0"],"y":["0.0","1.0"]}>>>to_json_obj(vect,Mapping[str,Tuple[Decimal,Decimal]]){"x":["1.0","0.0"],"y":["0.0","1.0"]}>>>to_json_obj(collections.OrderedDict(vect),...typing.OrderedDict[str,Tuple[Decimal,Decimal]])OrderedDict([("x",["1.0","0.0"]),("y",["0.0","1.0"])])Keys are either encoded or encoded and then stringified, depending on the key type:JSON basic types (bool,int,float,strandtype(None)) are encoded but not stringified;literal types are encoded but not stringified (because only JSON basic types are allowed as literals);enumeration types are encoded but not stringified (because they are already encoded as strings);all other types are first encoded and then stringified.For example, dictionaries using tuples as keys have their keys first encoded into lists and then stringified to form the keys of the final JSON dictionary:# Python 3.7.4>>>fromtypingimportDict,Tuple>>>fromtyping_jsonimportto_json_obj>>>to_json_obj({(0,1):"yes",(2,3):"no"},Dict[Tuple[int,int],str]){'[0, 1]':'yes','[2, 3]':'no'}JSON dictionaries are decoded byfrom_json_objto instances ofDictandMappingdepending on the specified type. JSON ordered dictionaries (collections.OrderedDict) are decoded byfrom_json_objto instances ofDict,MappingandOrderedDictdepending on the specified type.
Values and keys are recursively decoded, and keys are first de-serialised from strings (usingjson.loads) if they were stringified as part of the encoding.# Python 3.7.4>>>importcollections>>>fromdecimalimportDecimal>>>importtyping>>>fromtypingimportDict,Mapping,Tuple>>>fromtyping_jsonimportfrom_json_obj>>>vect_json={"x":["1.0","0.0"],"y":["0.0","1.0"]}>>>from_json_obj(vect_json,Dict[str,Tuple[Decimal,Decimal]]){"x":(Decimal("1.0"),Decimal("0.0")),"y":(Decimal("0.0"),Decimal("1.0"))}>>>from_json_obj(vect_json,Mapping[str,Tuple[Decimal,Decimal]]){"x":(Decimal("1.0"),Decimal("0.0")),"y":(Decimal("0.0"),Decimal("1.0"))}>>>from_json_obj(collections.OrderedDict(vect_json),...typing.OrderedDict[str,Tuple[Decimal,Decimal]])OrderedDict([("x",(Decimal("1.0"),Decimal("0.0"))),("y",(Decimal("0.0"),Decimal("1.0")))])>>>from_json_obj({'[0, 1]':'yes','[2, 3]':'no'},Dict[Tuple[int,int],str]){(0,1):"yes",(2,3):"no"}EnumerationsEnumerations members are encoded by using the corresponding member names and decoded by associating the number to the member of corresponding name:# Python 3.7.4>>>fromenumimportEnum>>>fromtyping_jsonimportto_json_obj,from_json_obj>>>classColor(Enum):...RED=(1.0,0.0,0.0)...GREEN=(0.0,1.0,0.0)...BLUE=(0.0,0.0,1.0)...>>>to_json_obj(Color.RED,Color)"RED">>>from_json_obj("RED",Color)<Color.RED:(1.0,0.0,0.0)>>>>to_json_obj({Color.RED:(255,0,0),...Color.GREEN:(0,255,0),...Color.BLUE:(0,0,255)},...Dict[Color,Tuple[int,int,int]]){'RED':[255,0,0],'GREEN':[0,255,0],'BLUE':[0,0,255]}Literal typesLiteral types can be constructed usingtyping_extensions.Literal, as long as the literals are all of JSON basic type.
Literal types are encoded/decoded exactly like JSON basic types would, i.e. nothing is done to them.Optional typesWhen encoding instances of anOptionaltype, it is first checked whether the instance can be encoded using the given generic type argument. If not, it is checked that the instance isNone, in which caseNoneis returned as the encoding (following the procedure for the JSON basic typetype(None)).# Python 3.7.4>>>fromtypingimportDict,Optional,Set>>>fromtyping_jsonimportto_json_obj>>>to_json_obj({"set":{1,2,3}},Dict[str,Optional[Set[int]]]){"set":[1,2,3]}>>>to_json_obj({"set":None},Dict[str,Optional[Set[int]]]){"set":None}Similarly, when decoding instances of anOptionaltype, it is first checked whether the JSON object can be decoded using the given generic type argument. If not, it is checked that the instance isNone, in which caseNoneis returned as the decoding.# Python 3.7.4>>>fromtypingimportDict,Optional,Set>>>fromtyping_jsonimportfrom_json_obj>>>from_json_obj({"set":[1,2,3]},Dict[str,Optional[Set[int]]]){"set":{1,2,3}}>>>from_json_obj({"set":None},Dict[str,Optional[Set[int]]]){"set":None}Union typesWhen serialising instancesobjof aUniontype, the generic type arguments ofUnionare tried in sequence until a typeTis found of whichobjis an instance (accoring to theis_instancefunction).
The serialisation then proceeds usingTas the static type:# Python 3.7.4>>>fromtypingimportUnion>>>fromtyping_jsonimportdumps>>>dumps(1,Union[int,str,float])# same as `dumps(1, int)`'1'>>>dumps("hello",Union[int,str,float])# same as `dumps("hello", str)`'"hello"'>>>dumps(2.5,Union[int,str,float])# same as `dumps(2.5, float)`'2.5'When the JSON dataobj_jsonis de-serialised, the generic type arugments ofUnionara again tried in sequence until a typeTis found which results in correct de-serialisation ofobj_json:# Python 3.7.4>>>fromtypingimportUnion>>>fromtyping_jsonimportloads>>>loads('1',Union[int,str,float])# same as `loads(1, int)`1>>>loads('"hello"',Union[int,str,float])# same as `loads("hello", str)`"hello">>>loads('2.5',Union[int,str,float])# same as `loads(2.5, float)`2.5This works well as long as the JSON encodings of the types are disjoint, as is the case for all JSON basic types.
Unfortunately, some issues arise with overlapping union types, explained more in detail below.
In short: if two types in aUnionhave overlapping JSON encodings (e.g.ListandSetare both encoded into JSON using lists), they may be deserialised to the incorrect runtime type (though the staticUniontype will still be respected).# Python 3.7.4>>>fromtypingimportList,Set,Union>>>fromtyping_jsonimportdumps,loads>>>UnionT=Union[List[int],Set[int]]>>>dumps([1,2,3],UnionT)'[1, 2, 3]'>>>dumps({1,2,3},UnionT)'[1, 2, 3]'>>>loads(dumps([1,2,3],UnionT),UnionT)[1,2,3]>>>loads(dumps({1,2,3},UnionT),UnionT)[1,2,3]Tagged unions can be used to mitigate this issue. Currently, tagged unions need to be defined manually (cf. below), but an automated way to construct them is a planned feature for future versions.Overlapping union typesHowever, this may create some issues when the following conditions are met:the JSON encodings for two type in theUnionoverlap, as is the case for the collectionsList,Tuple,Deque,SetandFrozenSet;the application depends on the runtime type of theUnioninstances in a way which results in incompatible behaviour on the overlaps.The de-serialised object is still going to be a valid instance of theUniontype, but its runtime type may not be the expected one.
To see a concrete example of this, imagine that we have a network with nodes labelled byint, featuring both directed and undirected edges.
The directed edges are encoded as 2-tuples, while the undirected edges are encoded as frozensets with two elements.
Let's look at what happens when we serialise and de-serialise such a network:# Python 3.7.4>>>fromtypingimportFrozenSet,NamedTuple,Set,Tuple,Union>>>fromtyping_jsonimportdumps,loads>>>classNetwork(NamedTuple):...nodes:Set[int]...edges:Set[Union[Tuple[int,int],FrozenSet[int]]]...>>>nodes={1,2,3}>>>edges={(1,2),frozenset({2,3}),frozenset({1,3})}>>>network=Network(nodes,edges)>>>print(network)Network(nodes={1,2,3},edges={(1,2),frozenset({1,3}),frozenset({2,3})})>>>network_serialised=dumps(network,Network)>>>print(network_serialised){"nodes":[1,2,3],"edges":[[1,2],[1,3],[2,3]]}>>>network_deserialised=loads(network_serialised,Network)>>>print(network_deserialised)Network(nodes={1,2,3},edges={(1,2),(1,3),(2,3)})Both directed edges (instances ofTuple[int, int]) and undirected edges (instances ofFrozenSet[int]with two elements) in ourNetworkdata structure are encoded as lists with two elements.
For example,[1, 3]is the encoding of both the undirected edgefrozenset({1, 3})(which is in our network) and a directed edge(1, 3)(which is not in our network).
Because of this,[1, 3]can be deserialised using bothTuple[int, int]andFrozenSet[int]: sinceTuple[int, int]appears first in the list of generic type arguments toUnion,[1, 3]will be deserialised to a directed edge(1, 3), even though it was serialised from an undirected edgefrozenset({1, 3}).
Indeed, you can see from the prints of the example above that our original network had one directed edge(1, 2)and two undirected edgesfrozenset({1, 3})andfrozenset({2, 3}), while the network we de-serialised has three directed edges(1, 2),(1, 2)and(2, 3)and no undirected edges.A way to solve this issue is to use a tagged union instead of the original union:# Python 3.7.4>>>fromtypingimportFrozenSet,NamedTuple,Set,Tuple,Union>>>fromtyping_extensionsimportLiteral>>>fromtyping_jsonimportdumps,loads>>>DirEdgeT=Tuple[Literal["d"],Tuple[int,int]]>>>UndirEdgeT=Tuple[Literal["u"],FrozenSet[int]]>>>classNetwork(NamedTuple):...nodes:Set[int]...edges:Set[Union[DirEdgeT,UndirEdgeT]]...@staticmethod...deffrom_untagged(nodes:Set[int],...edges:Set[Union[Tuple[int,int],FrozenSet[int]]])->Network:...tagged_edges={("d",e)ifisinstance(e,tuple)else("u",e)foreinedges}...returnNetwork(nodes,tagged_edges)...The factory methodfrom_untaggedis there to allow automated tagging of edges as directed/undirected based on their runtime type: it is not used when de-serialising the network objects from JSON.
Because the union is tagged, the edges are now de-serialised to the correct runtime type:# Python 3.7.4>>>nodes={1,2,3}>>>edges={(1,2),frozenset({2,3}),frozenset({1,3})}>>>network=Network.from_untagged(nodes,edges)>>>print(network)Network(nodes={1,2,3},edges={('u',frozenset({1,3})),('u',frozenset({2,3})),('d',(1,2))})>>>network_serialised=dumps(network,Network)>>>print(network_serialised){"nodes":[1,2,3],"edges":[["u",[1,3]],["u",[2,3]],["d",[1,2]]]}>>>network_deserialised=loads(network_serialised,Network)>>>print(network_deserialised)Network(nodes={1,2,3},edges={('u',frozenset({1,3})),('u',frozenset({2,3})),('d',(1,2))})