Package: json-strong-typing
Type-safe data interchange for PythonJSON is a popular message interchange format employed in API design for its simplicity, readability, flexibility and wide support. However,json.dumpandjson.loadoffer no direct support when working with Python data classes employing type annotations. This package offers services for working with strongly-typed Python classes: serializing objects to JSON, deserializing JSON to objects, and producing a JSON schema that matches the data class, e.g. to be used in an OpenAPI specification.Unlikeorjson, this package supports both serializing and deserializing complex types such as data classes, UUIDs, decimals, etc., and allows specifying custom serialization and deserialization hooks. It doesn't require introducing custom classes in your class inheritance chain (such asBaseModelinpydantic dataclasses), making it suitable for operating on classes defined in third-party modules.FeaturesThis package offers the following services:JSON serialization and de-serializationGenerate a JSON object from a Python object (serialization.object_to_json)Parse a JSON object into a Python object (serialization.json_to_object)JSON schemaGenerate a JSON schema from a Python type (schema.classdef_to_schema)Validate a JSON object against a Python type (schema.validate_object)Type informationExtract documentation strings (a.k.a. docstring) from types (docstring.parse_type)Inspect types, including generics (packageinspection)These services come with full support for complex types like data classes, named tuples and generics.In the context of this package, aJSON objectis the (intermediate) Python object representation produced byjson.loadsfrom aJSON string. In contrast, aJSON stringis the string representation generated byjson.dumpsfrom the (intermediate) Python object representation.Use casesWriting a cloud function (lambda) that communicates with JSON messages received as HTTP payload or websocket text messagesVerifying if an API endpoint receives well-formed inputGenerating a type schema for an OpenAPI specification to impose constraints on what messages an API can receive (seepython-openapi)Parsing JSON configuration files into a Python objectUsageConsider the following class definition:@dataclassclassExample:"A simple data class with multiple properties."bool_value:bool=Trueint_value:int=23float_value:float=4.5str_value:str="string"datetime_value:datetime.datetime=datetime.datetime(1989,10,23,1,45,50)guid_value:uuid.UUID=uuid.UUID("f81d4fae-7dec-11d0-a765-00a0c91e6bf6")First, we serialize the object to JSON withsource=Example()json_obj=object_to_json(source)Here, the variablejson_objhas the value:{"bool_value":True,"int_value":23,"float_value":4.5,"str_value":"string","datetime_value":"1989-10-23T01:45:50","guid_value":"f81d4fae-7dec-11d0-a765-00a0c91e6bf6",}Next, we restore the object from JSON withtarget=json_to_object(Example,json_obj)Here,targetholds the restored data class object:Example(bool_value=True,int_value=23,float_value=4.5,str_value="string",datetime_value=datetime.datetime(1989,10,23,1,45,50),guid_value=uuid.UUID("f81d4fae-7dec-11d0-a765-00a0c91e6bf6"),)We can also produce the JSON schema corresponding to the Python class:json_schema=json.dumps(classdef_to_schema(Example),indent=4)which yields{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"bool_value":{"type":"boolean","default":true},"int_value":{"type":"integer","default":23},"float_value":{"type":"number","default":4.5},"str_value":{"type":"string","default":"string"},"datetime_value":{"type":"string","format":"date-time","default":"1989-10-23T01:45:50"},"guid_value":{"type":"string","format":"uuid"}},"additionalProperties":false,"required":["bool_value","int_value","float_value","str_value","datetime_value","guid_value"],"title":"A simple data class with multiple properties."}If a type has a Python docstring, thentitleanddescriptionfields in the JSON schema are populated from the text in the documentation string.StandardsFor producing a JSON schema, the following JSON schema standards are supported:Draft 7Draft 2019-09Draft 2020-12Conversion tableThe following table shows the conversion types the package employs:Python typeJSON schema typeBehaviorNonenullboolbooleanintintegerfloatnumberstrstringdecimal.Decimalnumberbytesstringrepresented with Base64 content encodingdatetimestringconstrained to match ISO 8601 format2018-11-13T20:20:39+00:00datestringconstrained to match ISO 8601 format2018-11-13timestringconstrained to match ISO 8601 format20:20:39+00:00UUIDstringconstrained to match UUID formatf81d4fae-7dec-11d0-a765-00a0c91e6bf6Enumvalue typestores the enumeration value type (typically integer or string)Optional[T]depends on inner typereads and writesTif presentUnion[T1,T2, ...]depends on concrete typeserializes to the appropriate inner type; deserializes from the first matching typeList[T]arrayrecursive inTDict[K,V]objectrecursive inV, keys are coerced into stringDict[Enum,V]objectrecursive inV, keys are of enumeration value type and coerced into stringSet[T]arrayrecursive inT, container has uniqueness constraintTuple[T1,T2, ...]arrayarray has fixed length, each element has specific typeLiteral[const]type matchingconstexport the literal value as a constant valuedata classobjectiterates over fields of data classnamed tupleobjectiterates over fields of named tupleregular classobjectiterates overdir(obj)JsonArrayarrayuntyped JSON arrayJsonObjectobjectuntyped JSON objectAnyoneOfa union of all basic JSON schema typesAnnotated[T, ...]depends onToutputs value forT, applies constraints and format based on auxiliary type informationJSON schema examplesSimple basic typesPython typeJSON schemabool{"type": "boolean"}int{"type": "integer"}float{"type": "number"}str{"type": "string"}bytes{"type": "string", "contentEncoding": "base64"}Simple built-in typesPython typeJSON schemadecimal.Decimal{"type": "number"}datetime.date{"type": "string", "format": "date"}uuid.UUID{"type": "string", "format": "uuid"}Enumeration typesclassSide(enum.Enum):LEFT="L"RIGHT="R"{"enum":["L","R"],"type":"string"}Container typesPython typeJSON schemaList[int]{"type": "array", "items": {"type": "integer"}}Dict[str, int]{"type": "object", "additionalProperties": {"type": "integer"}}Set[int]{"type": "array", "items": {"type": "integer"}, "uniqueItems": True}}Tuple[int, str]{"type": "array", "minItems": 2, "maxItems": 2, "prefixItems": [{"type": "integer"}, {"type": "string"}]}Annotated typesRange:Annotated[int,IntegerRange(23,82)]){"type":"integer","minimum":23,"maximum":82,}Precision:Annotated[decimal.Decimal,Precision(9,6)]){"type":"number","multipleOf":0.000001,"exclusiveMinimum":-1000,"exclusiveMaximum":1000,}Fixed-width typesFixed-width integer (e.g.uint64) and floating-point (e.g.float32) types are annotated types defined in the packagestrong_typing.auxiliary. Their signature is recognized when generating a schema, and aformatproperty is written instead of minimum and maximum constraints.int32:int32=Annotated[int,Signed(True),Storage(4),IntegerRange(-2147483648,2147483647)]{"format":"int32","type":"integer"}uint64:uint64=Annotated[int,Signed(False),Storage(8),IntegerRange(0,18446744073709551615)]{"format":"uint64","type":"integer"}Any type{"oneOf":[{"type":"null"},{"type":"boolean"},{"type":"number"},{"type":"string"},{"type":"array"},{"type":"object"},]}Custom serialization and de-serializationIf a composite object (e.g. a dataclass or a plain Python class) has ato_jsonmember function, then this function is invoked to produce a JSON object representation from an instance.If a composite object has afrom_jsonclass function (a.k.a.@classmethod), then this function is invoked, passing the JSON object as an argument, to produce an instance of the corresponding type.Custom typesIt is possible to declare custom types when generating a JSON schema. For example, the following class definition has the annotation@json_schema_type, which will register a JSON schema subtype definition under the path#/definitions/AzureBlob, which will be referenced later with$ref:_regexp_azure_url=re.compile(r"^https?://([^.]+)\.blob\.core\.windows\.net/([^/]+)/(.*)$")@dataclass@json_schema_type(schema={"type":"object","properties":{"mimeType":{"type":"string"},"blob":{"type":"string","pattern":_regexp_azure_url.pattern,},},"required":["mimeType","blob"],"additionalProperties":False,})classAzureBlob(Blob):...You can use@json_schema_typewithout theschemaparameter to register the type name but have the schema definition automatically derived from the Python type. This is useful if the type is reused across the type hierarchy:@json_schema_typeclassImage:...classStudy:left:Imageright:ImageHere, the two properties ofStudy(leftandright) will refer to the same subtype#/definitions/Image.Union typesSerializing a union type entails serializing the active member type.De-serializing discriminated (tagged) union types is based on a disjoint set of property values with type annotationLiteral[...]. Consider the following example:@dataclassclassClassA:name:Literal["A","a"]value:str@dataclassclassClassB:name:Literal["B","b"]value:strHere, JSON representations ofClassAandClassBare indistinguishable based on property names alone. However, the propertynameforClassAcan only take values"A"and"a", and propertynameforClassBcan only take values"B"and"b", hence a JSON object such as{"name":"A","value":"string"}uniquely identifiesClassA, and can never matchClassB. The de-serializer can instantiate the appropriate class, and populate properties of the newly created instance.Tagged union types must have at least one property of a literal type, and the values for that type must be all different.When de-serializing regular union types that have no type tags, the first successfully matching type is selected. It is a parse error if all union member types have been exhausted without a finding match.Name manglingIf a Python class has a property augmented with an underscore (_) as perPEP 8to avoid conflict with a Python keyword (e.g.fororin), the underscore is removed when reading from or writing to JSON.