The goal of jsontemplate
is to allow developers to define JSON templates in Python in a simple and elegant manner.
Here a the most important features of jsontemplate:
- Pure python template definition
- JSON file structure validation
- Generation of example JSON file and Python dict from a template
- Optional fields
- Default values
- Casting to custom Python objects
- Mixin types
- Lists with or without a constrained number of elements
- Strict mode (no extra keys and no casting)
- Enumerators
Here's what a simple template looks like:
import json
from jsontemplate import template, optional
config_template = template({
"first_name": str,
"last_name": str,
"age": int,
"animals": optional([
{
"name": str,
"age": int,
"specie": str
}
]),
"location": (str, int),
"scores": [{float, int}], # {float, int} is a type mixin
"some_array": [float, int]
})
with open('./config.json', 'rb') as jsonfile:
config = json.load(jsonfile)
# raises an exception if config doesn't respect the template
config_template.validate(config)
What it means:
- The
first_name
andlast_name
fields of the JSON file must be strings - The
age
field of the JSON file must castable to an integer without loss of information - If the
animals
field is defined, then it must be a list of objects containing at least the fieldsname
,age
, andspecie
- The
location
field must be a list of exactly two elements: the first must be a string, the second an integer - The
scores
field must be a list of floats or integers that can be mixed - The
some_array
field must be a list containing either only float, or only integers
Note: In Python 2.7 str
will automatically be replaced by unicode
for JSON compliance.
With the previously defined template we can do the following:
>>> config_template.example()
>>> {
"first_name": u'example',
"last_name": u'example',
"age": 0,
"location": (u'example', 0),
"scores": [0.0], # or [0]
"some_array": [0.0]
}
>>> config_template.example(full=True)
>>> {
"first_name": u'example',
"last_name": u'example',
"age": 0,
"animals": [
{
"name": u'example',
"age": 0,
"specie": u'example'
}
],
"location": [u'example', 0],
"scores": [0.0], # or [0]
"some_array": [0.0]
}
Let's modify (and simplify) our template a little:
>>> config_template = template({
"first_name": str,
"last_name": str,
"age": default(int, 42)
})
>>> config_template.example()
>>> {
'first_name': u'example',
'last_name': u'example',
'age': 42,
}
>>> config_template.output({
'first_name': u'Adrien',
'last_name': u'El Zein'})
>>> {
'first_name': u'Adrien',
'last_name': u'El Zein',
'age': 42
}
Note: it is possible to simply write 42
instead of default(int, 42)
, the type will be infered from the value.
By passing strict=True
to the template
factory, or in the validate
and output
methods,
the template will not accept extra keys in the json file and will enforce the types
instead of checking that the values are castable.
It is also possible to use the strict modules only on sub-dictionaries with the strict
keyword:
from jsontemplate import template, strict
t = template({
"first_name": str,
"last_name": str,
'pokemon': strict({
'name': str,
'hp': int,
})
})
data = {
'first_name': u'Adrien',
'last_name': u'El Zein',
'age': 42,
'pokemon': {
'name': u'pikachu',
'hp': 42,
'age': 2,
}
}
t.validate(data) # will fail
data = {
'first_name': u'Adrien',
'last_name': u'El Zein',
'age': 42,
'pokemon': {
'name': u'pikachu',
'hp': 42,
}
}
t.validate(data) # will pass
data = {
'first_name': u'Adrien',
'last_name': u'El Zein',
'age': 42,
'pokemon': {
'name': u'pikachu',
'hp': 42,
}
}
t.validate(data, strict=True) # will fail
t = template({
"first_name": str,
"last_name": str,
'pokemon': strict({
'name': str,
'hp': int,
})
}, strict=True)
t.validate(data) # will also fail
It is possible to cast the Python native types of a converted JSON file into more complex and/or custom-defined Python objects.
from uuid import UUID
def uuid(integer):
return UUID(int=integer)
from jsontemplate import template, cast, starcast, kwcast
class Animal:
def __init__(self, name, specie, age):
self.name = name
self.specie = specie
self.age = age
def some_method(self):
pass
config_template = template({
"id": cast(uuid, source=int), # the first argument of cast doesn't have to be a type, a callable will work too
"animals": [starcast(Animal, source=(str, str, int))], # Animal(*('string', 'string', integer)) will be called
"id2": kwcast(UUID, source={'hex': str}) # UUID(**dict(hex='string')) will be called
})
print config_template.output({
"id": 343,
"animals": [(u'kupa', u'cat', 12)],
"id2": u'12344532323473451234453232347345'
})
This script will print the following:
>>> {
'id': UUID('00000000-0000-0000-0000-000000000157'),
'animals': [<__main__.Animal instance at 0x000000000>],
'id2': UUID('12344532-3234-7345-1234-453232347345')
}
It is possible to define more complex mixin types than with a simple set, the latter being limited by its inability to contain non-hashable templates.
from jsontemplate import template, mixin
config_template = template({
"first_name": str,
"last_name": str,
"age": int,
"animal": mixin({
"name": str,
"age": int,
"specie": str},
(str, str, int))
})
The animal
field in this template accepts a dictionary or a tuple. This behavior would be impossible to obtain with the set notation for mixins, since dicts can't be elements of sets.
Note: with this notation, the template will also have the nice behavior to try the types in the given order and stop at the first that works.
This behavior would not have been guaranteed with sets because they don't conserve order either
It is possible to check if a list has an number of elements between a min and a max:
from jsontemplate import template, size
config_template = template({
"first_name": str,
"last_name": str,
"age": int,
"animals": size([{
"name": str,
"age": int,
"specie": str
}], min=1, max=5)
})
The animals
field can only contain a list containing at least 1 element and at most 5 elements. min
defaults to 0 and if max
is not present, the list length has no upper limit.