JSON Schema Mapping

The Basics - Usage

Typedpy allows to map a json draft4 schema to code. This code can be saved to a .py file (recommended) or executed dynamically. It supposts references to schema definitions, It also creates definitions whenever a Structure is being referenced. Mapping a JSON schema to code, inherently provides schema validation when the generated classes are used.

from typedpy import schema_definitions_to_code, schema_to_struct_code, write_code_from_schema
from typedpy.structures import *
from typedpy.fields import *

definitions = {
"SimpleStruct": {
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "pattern": "[A-Za-z]+$",
            "maxLength": 8
        }
    },
    "required": [
        "name"
    ],
    "additionalProperties": True
    }
}

schema = {
    "type": "object",
    "description": "This is a test of schema mapping",
    "properties": {
        "foo": {
            "type": "object",
            "properties": {
                "a2": {
                    "type": "float"
                },
                "a1": {
                    "type": "integer"
                }
            },
            "required": [
                "a2",
                "a1"
            ],
            "additionalProperties": True
        },
        "ss": {
            "$ref": "#/definitions/SimpleStruct"
        },
        "enum": {
            "enum": [
                1,
                2,
                3
            ]
        },
        "s": {
            "maxLength": 5,
            "type": "string"
        },
        "i": {
            "type": "integer",
            "maximum": 10
        },
        "all": {
            "allOf": [
                {
                  "type": "number"
                },
                {
                 "type": "integer"
                }
            ]
        },
        "a": {
        "type": "array",
            "items": [
                {
                    "type": "integer",
                    "multiplesOf": 5
                },
                {
                    "type": "number"
                }
            ]
        }
    },
    "required": [
        "foo",
        "ss",
        "enum",
        "s",
        "i",
        "all",
        "a"
    ],
    "additionalProperties": True,
}

definitions_code = schema_definitions_to_code(definitions)
exec(definitions_code, globals())

struct_code = schema_to_struct_code('Duba', schema, definitions)
exec(struct_code, globals())
duba = Duba(
    foo = {'a1': 5, 'a2': 1.5},
    ss = SimpleStruct(name = 'abc'),
    enum = 2,
    s = "xyz",
    i = 10,
    all = 6,
    a = [10, 3]
)
assert duba.ss.name == 'abc'

# Alternatively, and preferable, save it to a file:
write_code_from_schema(schema, definitions, "generated_sample.py", "Poo")
# Now we can load and use it:
from generated_sample import Poo, SimpleStruct

Non-object top-level schemas are also supported, using a “field wrapper”. A field wrapper Structure is a Structure that contains a single, required, field, and does not allow any other property (i.e. _additionalProperties = False). For example:

For example:

class Foo(Structure):
    arr = Array(minItems=2)
    _required = ['arr']
    _additional_properties = False

schema, definitions = structure_to_schema(Foo, {})
assert schema == {
    "type": "array",
    "minItems": 2
}

# And the back to a Structure...
struct_code = schema_to_struct_code('Bar', schema, {})
exec(struct_code, globals())

bar = Bar([1,2,3])
assert bar.wrapped[2] == 3

In the example above, if ‘_required’ was [], or _additional_properties was True, then the schema was an object with a single property ‘arr’, as usual.

Schema mapping beyond the “basic” Types

To support custom Field types mapping, you need to implement 2 methods in the Field class: to_json_schema, and from_json_schema. Here is an example from Typedpy:

class IPV4(String):
    # skipping some code....

    @classmethod
    def to_json_schema(cls) -> dict:
        return {"type": "string", "format": "ipv4"}

    @classmethod
    def from_json_schema(cls, schema: dict):
        return "IPV4()" if schema == IPV4.to_json_schema() else None
  1. to_json_schema - returns the schema for this field.

  2. from_json_schema - accepts the schema and returns the string for the code needed to instantiate the Field. If it does not match, it should return None.

  3. When converting from schema to code, the optional parameter “additional_fields” should have a list of all the classes with custom mapping

Limitations and Comments

  1. Not all the details of JSON schema are supported - but most of the common ones are.

  2. Only the Field types that map to JSON Schema Draft 4 are inherently supported. This means that if you add a new custom Field class, you need to use the method described above.

  3. Set and Tuple fields are mapped to array types when converting code to schema

  4. Regarding JSON pointers(i.e. “$ref”) - only pointers that point to an object under “#/definitions/” are supported

Examples

Take a look at the many tests here .

Functions

Code to schema

structure_to_schema(structure, definitions_schema, serialization_mapper=None)[source]

Generate JSON schema from Structure See working examples in tests.

Arguments:
structure( subclass of Structure ):

the class

definitions_schema(dict):

the json schema for all the definitions (typically under “#/definitions” in the schema. If it is the first call, just use and empty dict.

Returns:

A tuple of 2. The fist is the schema of structure, the second is the schema for the referenced definitions. the The schema that the code maps to. It also updates

Keep in mind that in order to print a JSON schema (as opposed to a Python dict), you need to do something like:

print(json.dumps(schema, indent=4))

Schema to code

schema_definitions_to_code(schema, additional_fields=<class 'list'>)[source]

Generate code for the classes in the definitions that maps to the given JSON schema. `See working example in test_schema_to_code.py.

Arguments:
schema(dict):

the json schema of the various Structures that need to be defined

Returns:

A string with the code. This can either be executed directly, using exec(), or written to a file. If you write to a file, the higher level write_code_from_schema() is preferable.

schema_to_struct_code(struct_name, schema, definitions_schema, additional_fields=<class 'list'>)[source]

Generate code for the main class that maps to the given JSON schema. The main struct_name can include references to structures defined in definitions_schema, under “#/definitions/”.

Arguments:
struct_name(str):

the name of the main Structure to be created

schema(dict):

the json schema of the main Structure that need to be defined

definitions_schema(dict):

schema for definitions of objects that can be referred to in the main schema. If non exist, just use an empty dict.

additional_fields(list):

additional Types of Fields with custom schema mapping that can appear in the schema. These have to implement the class method from_json_schema(), which should return a string of the code the Schema is mapping to.

Returns:

A string with the code of the class. This can either be executed directly, using exec(), or written to a file. The “description” property, if exists, is mapped to the docstring of the class. If you write to a file, the higher level write_code_from_schema() is preferable. Note: In case schema is None, should return None. Deals with a schema that is a dict, as well as one that is a list

write_code_from_schema(schema, definitions_schema, filename, class_name, additional_fields=<class 'list'>)[source]

Generate code from schema and write it to a file.

Example:

write_code_from_schema(
    schema,
    definitions,
    "generated_sample.py",
    "Poo",
     additional_fields=[CustomField1, CustomField2]
)
Arguments:
schema(dict):

the json schema for the main structure

definitions_schema(dict):

the json schema for all the definitions (typically under “#/definitions” in the schema. These can be referred to from the main schema

filename(str):

the file name for the output. Typically should be end with “.py”.

class_name(str):

the main Structure name

additional_fields(list[Type[Field]]):

additional field classes with custom mapping