Package: jsonschema-typed2
JSON Schema-powered type annotationsThe goal of this project is to use JSON Schema for type checking in Python.While there is not a perfect 1:1 mapping between concepts in JSON Schema and
Python's typing system, there is enough of an isomorphism to warrant some
exploration of the possibilities. Since a JSON document is generally
represented as adictin Python programs, this project looks specifically
at interpreting JSON schema asTypedDictdefinitions.Warning:there are bound to be some abuses of themypy plugin
systemhere. You
have been warned.This leverages (and is inspired by)https://github.com/Julian/jsonschema.OverviewA JSON schema:{"$schema":"http://json-schema.org/draft-07/schema#","$id":"http://foo.qwerty/some/schema#","title":"Foo Schema","type":"object","properties":{"title":{"type":"string"},"awesome":{"type":"number"}},"required":["title"]}A TypedDict:fromjsonschema_typed.typesimportJSONSchemadata:JSONSchema['path/to/schema.json']=dict(title='baz')data['description']='there is no description'# TypedDict "FooSchema" has no key 'description'data['awesome']=42data['awesome']=None# Argument 2 has incompatible type "None"; expected "Union[int, float]"Installationpipinstalljsonschema-typedorgitclonegit@github.com:erickpeirson/jsonschema-typed.gitcdjsonschema-typed
pythonsetup.pyinstallRequirementsSo far I have only tried this with:mypy==0.701jsonschema==3.0.1But probably older versions work. You could try it out andlet me know.LimitationsadditionalPropertiesdoesn't really have an equivalent in TypedDict. Yet.Cases in which the root of the schema is anything other than anobjectare not terribly interesting for this project, so we ignore them for now.
Array values fortype(e.g."type": ["object", "boolean"]) are
otherwise supported withUnion.Thedefaultkeyword is not supported; but see:https://github.com/python/mypy/issues/6131.Self-references (e.g."#") can't really work properly until nested
forward-references are supported; seehttps://github.com/python/mypy/issues/731.There are probably others.ApproachSo far two approaches are attempted:Annotating adictinstance that will be aTypedDictthat conforms to
the JSON Schema (as best we can enforce it).Using a dynamic base class that is typed as aTypedDict.Both examples below use the schema:{"$schema":"http://json-schema.org/draft-07/schema#","$id":"http://foo.qwerty/some/schema#","title":"Foo Schema","type":"object","properties":{"title":{"type":"string"},"awesome":{"type":"number"}}}First approach: annotating adictThis has the advantage of being fairly simple. It is implemented viajsonschema_typed.plugin.JSONSchemaPlugin.get_type_analyze_hook.fromjsonschema_typed.typesimportJSONSchemadata:JSONSchema['path/to/schema.json']=dict(title='baz')reveal_type(data)# Revealed type is 'TypedDict('FooSchema', {'title'?: builtins.str, 'awesome'?: Union[builtins.int, builtins.float]})'data['description']='there is no description'# TypedDict "FooSchema" has no key 'description'data['awesome']=42data['awesome']=None# Argument 2 has incompatible type "None"; expected "Union[int, float]"Here is the mypy output:main.py:4: error: Revealed type is 'TypedDict('FooSchema', {'title'?: builtins.str, 'awesome'?: Union[builtins.int, builtins.float]})'
main.py:5: error: TypedDict "FooSchema" has no key 'description'
main.py:7: error: Argument 2 has incompatible type "None"; expected "Union[int, float]"Note that the right-hand side can be adictor a subclass ofdict, so
you could define a subclass like:classFoo(dict):"""Some domain logic on your object."""defdo_something(self,arg:int)->int:"""Do something awesome."""returnarg*self['awesome']data:JSONSchema['schema/test.json']=Foo(title='baz')reveal_type(data)# Revealed type is 'TypedDict('FooSchema', {'title'?: builtins.str, 'awesome'?: Union[builtins.int, builtins.float]})'data['description']='there is no description'# TypedDict "FooSchema" has no key 'description'data['awesome']=42data['awesome']=None# Argument 2 has incompatible type "None"; expected "Union[int, float]"Of course this isn't quite consistent with PEP-589 whichstatesthat:Methods are not allowed, since the runtime type of a TypedDict object will
always be just dict (it is never a subclass of dict).So use at your own risk.Second approach: dynamic base classThis has the advantage of being able to add some runtime-functionality, e.g.
usejsonschemato actually validate data at runtime. It is implemented viajsonschema_typed.plugin.JSONSchemaPlugin.get_dynamic_class_hook.But again, this isn't quite consistent with PEP-589 whichstates, so
use at your own risk.fromjsonschema_typed.typesimportJSONSchemaBaseBase=JSONSchemaBase('path/to/schema.json')classFoo(Base):"""All your base in one place."""defdo_something(self,arg:int)->int:"""Do something awesome."""returnarg*self['awesome']data=Foo(title='baz')reveal_type(data)# Revealed type is 'TypedDict('FooSchema', {'title'?: builtins.str, 'awesome'?: Union[builtins.int, builtins.float]})'data['description']='there is no description'# TypedDict "FooSchema" has no key 'description'data['awesome']=42data['awesome']=None# Argument 2 has incompatible type "None"; expected "Union[int, float]"TODODecide whether to stick with just one approach (and which one)Write some testsTest against other versions of mypy + jsonschema