Quickstart¶
This guide will walk you through the basics of creating Gnarl schemas.
Declaring Schemas¶
Let’s start with a basic “user” model. Inheriting from Schemed
allows
to define a schema:
>>> import gnarl
>>> class User(gnarl.Schemed):
... __schema__ = {
... "name" : str,
... "email" : str, # Not a very thorough validation
... "created_at" : gnarl.Timestamp,
... }
... def __repr__(self):
... return "<User name={u.name!r}>".format(u=self)
...
The __schema__
class attribute is a dictionary that defines the names
of the attributes of User
instances, and their types.
We could have defined the email
attribute in such a way that the e-mail
address would be checked for validity, but we will leave that for later.
Object Instantiation¶
We can instantiate User
normally, as long as the parameters to the
constructor are given as keyword arguments. The timestamp can be passed as a
string, and it will be parsed using the
Delorean module:
>>> jdoe = User(name="John Doe", email="jdoe@spammail.com",
... created_at="1983-05-11 19:35:00 +0000")
...
>>> jdoe
<User name='John Doe'>
Note how the attributes declared in the schema are available using the normal attribute syntax; the timestamp has been converted automatically into an instance of the Delorean class:
>>> jdoe.name
'John Doe'
>>> jdoe.email
'jdoe@spammail.com'
>>> jdoe.created_at
Delorean(datetime=datetime.datetime(1983, 5, 11, 19, 35), timezone='UTC')
The attributes can be modified normally as well:
>>> jdoe.email = "johnny@doe.org"
>>> jdoe.email
'johnny@doe.org'
Data Validation¶
Data contained in schema attributes are guaranteed to always contain valid
information of declared type. This means that assigning values of invalid
types to attributes will raise a SchemaError
:
>>> jdoe.name = 32
Traceback (most recent call last):
...
gnarl.SchemaError: 32 should be instance of <class 'str'>
Validation will be also carried on at object instantiation:
>>> User(name=32, email="a@b.com", created_at="2015-09-30")
...
Traceback (most recent call last):
...
gnarl.SchemaError: 32 should be instance of <class 'str'>
JSON Serialization¶
Objects can be serialized to JSON using the
Schemed.to_json()
method, which accepts the same keyword arguments as
the json.dumps()
function from the standard library:
>>> print(jdoe.to_json(sort_keys=True, indent=4))
{
"created_at": "1983-05-11T19:35:00+00:00",
"email": "johnny@doe.org",
"name": "John Doe"
}
Conversely, the Schemed.from_json()
class method will do the opposite,
deserializing a JSON string and creating objects as needed:
>>> u = User.from_json("""\
... { "name": "Monty", "email": "monty@python.org",
... "created_at": "1991-10-11T20:00:00+00:00" }""")
...
>>> u
<User name='Monty'>
When deserializing data from JSON, input validation and conversion is done exactly in the same way, always following the declared schema.
HiPack Serialization¶
If you have the hipack module
installed (it is an optional dependency, Gnarl will work just fine without
it), it is also possible to serialize objects to HiPack, using the Schemed.to_hipack()
method.
Deserialization and validation can be done using the
Schemed.from_hipack()
class method.
Collections¶
Schemas may contain nested lists and dictionaries. Let’s change our User
class to allow multiple e-mail addresses:
>>> class User(gnarl.Schemed):
... __schema__ = {
... "name": str,
... "emails": [str], # A list of strings.
... }
...
>>> jdoe = User(name="John Doe",
... emails=["jdoe@spammail.com", "john@doe.org"])
...
>>> jdoe.emails
['jdoe@spammail.com', 'john@doe.org']
Dictionaries work as expected, but note that all keys and the types of their associated values are fully type-checked:
>>> class User(gnarl.Schemed):
... __schema__ = { "name": { "first": str, "family": str } }
...
>>> jdoe = User(name=dict(first="John", family="Doe"))
>>> sorted(jdoe.name.items())
[('family', 'Doe'), ('first', 'John')]
Better Validation¶
Remember that e-mail addresses were not being verified for correctness? Gnarl can automate additional validation for us as well. First, let’s define a validation function for e-mail addresses:
>>> def validate_email(email):
... if "@" not in email: # Naïve check
... raise gnarl.SchemaError("{!r} does not contain @".format(email))
... return email
...
The gnarl.Use
helper class can be used to wrap a validation function
and use it as part of the schema. We still want to ensure that the value is a
string, and so gnarl.And
is used to instruct the validation engine to
ensure that the value is a string, and that the validation function does not
raise an error:
>>> class User(gnarl.Schemed):
... __schema__ = {
... "name": str,
... "email": gnarl.And(str, validate_email),
... }
...
Now, using an invalid e-mail address will result in an error, even if the value is a string:
>>> jdoe = User(name="John Doe", email="invalid address")
Traceback (most recent call last):
...
gnarl.SchemaError: 'invalid address' does not contain @
Nesting Schemas¶
It is possible to use a subclass of gnarl.Schemed
as an schema type
itself. This allows to construct schemas in which attributes can be themselves
type-checked objects. In our example, we could define the name
attribute
to be an object with separate attributes for the surname and the family name:
>>> class Name(gnarl.Schemed):
... __schema__ = { "first": str, "family": str }
...
>>> class User(gnarl.Schemed):
... __schema__ = { "name": Name, "email": str }
...
Instantiating objects gets a little bit more involved, though the way things work is still logical:
>>> jdoe = User(name=Name(first="John", family="Doe"),
... email="j@doe.org")
...
Serialization of nested schemas works as expected, using nested JSON dictionaries for the child objects:
>>> print(jdoe.to_json(sort_keys=True, indent=4))
{
"email": "j@doe.org",
"name": {
"family": "Doe",
"first": "John"
}
}
Loading a JSON snippet also works as expected when using nested schemas:
>>> monty = User.from_json("""\
... { "email": "monty@spam.org", "name": {
... "first": "Monty", "family": "Python" }}""")
...
>>> isinstance(monty.name, Name)
True
>>> monty.email, monty.name.first, monty.name.family
('monty@spam.org', 'Monty', 'Python')