jsonweb.decode – decode your python classes

Sometimes it would be nice to have json.loads() return class instances. For example if you do something like this

person = json.loads('''
{
    "__type__": "Person",
    "first_name": "Shawn",
    "last_name": "Adams"
}
''')

it would be pretty cool if instead of person being a dict it was an instance of a class we defined called Person. Luckily the python standard json module provides support for class hinting in the form of the object_hook keyword argument accepted by json.loads().

The code in jsonweb.decode uses this object_hook interface to accomplish the awesomeness you are about to witness. Lets turn that person dict into a proper Person instance.

>>> from jsonweb.decode import from_object, loader

>>> @from_object()
... class Person(object):
...     def __init__(self, first_name, last_name):
...         self.first_name = first_name
...         self.last_name = last_name
...

>>> person = loader('''
... {
...     "__type__": "Person",
...     "first_name": "Shawn",
...     "last_name": "Adams"
... }
... ''')

>>> print type(person)
<class 'Person'>
>>> print person.first_name
"Shawn"

But how was jsonweb able to determine how to instantiate the Person class? Take a look at the from_object() decorator for a detailed explanation.

loader

jsonweb.decode.loader(json_str, **kw)

Call this function as you would call json.loads(). It wraps the Object hook interface and returns python class instances from JSON strings.

Parameters:
  • ensure_type – Check that the resulting object is of type ensure_type. Raise a ValidationError otherwise.
  • handlers – is a dict of handlers. see object_hook().
  • as_type – explicitly specify the type of object the JSON represents. see object_hook()
  • validate – Set to False to turn off validation (ie dont run the schemas) during this load operation. Defaults to True.
  • kw – the rest of the kw args will be passed to the underlying json.loads() calls.

Decorators

jsonweb.decode.from_object(handler=None, type_name=None, schema=None)

Decorating a class with from_object() will allow json.loads() to return instances of that class.

handler is a callable that should return your class instance. It receives two arguments, your class and a python dict. Here is an example:

>>> from jsonweb.decode import from_object, loader
>>> def person_decoder(cls, obj):
...    return cls(
...        obj["first_name"],
...        obj["last_name"]
...    )
...
>>> @from_object(person_decoder)        
... class Person(object):
...     def __init__(self, first_name, last_name):
...         self.first_name
...         self.last_name = last_name
...
>>> person_json = '{"__type__": "Person", "first_name": "Shawn", "last_name": "Adams"}'
>>> person = loader(person_json)
>>> person
<Person object at 0x1007d7550>
>>> person.first_name
'Shawn'

The __type__ key is very important. Without it jsonweb would not know which handler to delegate the python dict to. By default from_object() assumes __type__ will be the class’s __name__ attribute. You can specify your own value by setting the type_name keyword argument

@from_object(person_decoder, type_name="PersonObject")

Which means the json string would need to be modified to look like this:

'{"__type__": "PersonObject", "first_name": "Shawn", "last_name": "Adams"}'

If a handler cannot be found for __type__ an exception is raised

>>> luke = loader('{"__type__": "Jedi", "name": "Luke"}')
Traceback (most recent call last):
    ...
ObjectNotFoundError: Cannot decode object Jedi. No such object.

You may have noticed that handler is optional. If you do not specify a handler jsonweb will attempt to generate one. It will inspect your class’s __init__ method. Any positional arguments will be considered required while keyword arguments will be optional.

Warning

A handler cannot be generated from a method signature containing only *args and **kwargs. The handler would not know which keys to pull out of the python dict.

Lets look at a few examples:

>>> from jsonweb import from_object
>>> @from_object()
... class Person(object):
...     def __init__(self, first_name, last_name, gender):
...         self.first_name = first_name
...         self.last_name = last_name
...         self.gender = gender

>>> person_json = '{"__type__": "Person", "first_name": "Shawn", "last_name": "Adams", "gender": "male"}'
>>> person = loader(person_json)

What happens if we dont want to specify gender:

>>> person_json = '''{
...     "__type__": "Person", 
...     "first_name": "Shawn", 
...     "last_name": "Adams"
... }'''
>>> person = loader(person_json)
Traceback (most recent call last):
    ...        
ObjectAttributeError: Missing gender attribute for Person.

To make gender optional it must be a keyword argument:

>>> from jsonweb import from_object
>>> @from_object()
... class Person(object):
...     def __init__(self, first_name, last_name, gender=None):
...         self.first_name = first_name
...         self.last_name = last_name
...         self.gender = gender

>>> person_json = '{"__type__": "Person", "first_name": "Shawn", "last_name": "Adams"}'
>>> person = loader(person_json)
>>> print person.gender
None

You can specify a json validator for a class with the schema keyword agrument. Here is a quick example:

>>> from jsonweb import from_object
>>> from jsonweb.schema import ObjectSchema
>>> from jsonweb.validators import ValidationError, String
>>> class PersonSchema(ObjectSchema):
...    first_name = String()
...    last_name = String()
...    gender = String(optional=True)
...
>>> @from_object(schema=PersonSchema)
... class Person(object):
...     def __init__(self, first_name, last_name, gender=None):
...         self.first_name = first_name
...         self.last_name = last_name
...         self.gender = gender
...
>>> person_json = '{"__type__": "Person", "first_name": 12345, "last_name": "Adams"}'
>>> try:
...     person = loader(person_json)
... except ValidationError, e:
...     print e.errors["first_name"].message
Expected str got int instead.

Schemas are useful for validating user supplied json in web services or other web applications. For a detailed explanation on using schemas see the jsonweb.schema.

Object hook

jsonweb.decode.object_hook(handlers=None, as_type=None, validate=True)

Wrapper around ObjectHook. Calling this function will configure an instance of ObjectHook and return a callable suitable for passing to json.loads() as object_hook.

If you need to decode a JSON string that does not contain a __type__ key and you know that the JSON represents a certain object or list of objects you can use as_type to specify it

>>> json_str = '{"first_name": "bob", "last_name": "smith"}'
>>> loader(json_str, as_type="Person")
<Person object at 0x1007d7550>
>>> # lists work too
>>> json_str = '''[
...     {"first_name": "bob", "last_name": "smith"},
...     {"first_name": "jane", "last_name": "smith"}
... ]'''
>>> loader(json_str, as_type="Person")
[<Person object at 0x1007d7550>, <Person object at 0x1007d7434>]

Note

Assumes every object WITHOUT a __type__ kw is of the type specified by as_type .

handlers is a dict with this format:

{"Person": {"cls": Person, "handler": person_decoder, "schema": PersonSchema)}

If you do not wish to decorate your classes with from_object() you can specify the same parameters via the handlers keyword argument. Here is an example:

>>> class Person(object):
...    def __init__(self, first_name, last_name):
...        self.first_name = first_name
...        self.last_name = last_name
...        
>>> def person_decoder(cls, obj):
...    return cls(obj["first_name"], obj["last_name"])
    
>>> handlers = {"Person": {"cls": Person, "handler": person_decoder}}
>>> person = loader(json_str, handlers=handlers)
>>> # Or invoking the object_hook interface ourselves
>>> person = json.loads(json_str, object_hook=object_hook(handlers))

Note

If you decorate a class with from_object() you can override the handler and schema values later. Here is an example of overriding a schema you defined with from_object() (some code is left out for brevity):

>>> from jsonweb import from_object
>>> @from_object(schema=PersonSchema)
>>> class Person(object):
    ...
    
>>> # and later on in the code...
>>> handlers = {"Person": {"schema": NewPersonSchema}}
>>> person = loader(json_str, handlers=handlers)

If you need to use as_type or handlers many times in your code you can forgo using loader() in favor of configuring a “custom” object hook callable. Here is an example

>>> my_obj_hook = object_hook(handlers)
>>> # this call uses custom handlers
>>> person = json.loads(json_str, object_hook=my_obj_hook)
>>> # and so does this one ...
>>> another_person = json.loads(json_str, object_hook=my_obj_hook)                                
class jsonweb.decode.ObjectHook(handlers, validate=True)

This class does most of the work in managing the handlers that decode the json into python class instances. You should not need to use this class directly. object_hook() is responsible for instantiating and using it.

decode_obj(obj)

This method is called for every dict decoded in a json string. The presence of the key __type__ in obj will trigger a lookup in self.handlers. If a handler is not found for __type__ then an ObjectNotFoundError is raised. If a handler is found it will be called with obj as it only argument. If an ObjectSchema was supplied for the class, obj will first be validated then passed to handler. The handler should return a new python instant of type __type__.

as_type context mananger

jsonweb.decode.ensure_type(*args, **kwds)

This context manager lets you “inject” a value for ensure_type into loader() calls made in the active context. This will allow a ValidationError to bubble up from the underlying loader() call if the resultant type is not of type ensure_type.

Here is an example

# example_app.model.py
from jsonweb.decode import from_object
# import db model stuff
from example_app import db

@from_object()
class Person(db.Base):
    first_name = db.Column(String)
    last_name = db.Column(String)
    
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name

        
# example_app.__init__.py
from example_app.model import session, Person
from jsonweb.decode import from_object, ensure_type
from jsonweb.schema import ValidationError
from flask import Flask, request, abort

app.errorhandler(ValidationError)
def json_validation_error(e):
    return json_response({"error": e})
    
    
def load_request_json():
    if request.headers.get('content-type') == 'application/json':
        return loader(request.data)
    abort(400)
    
    
@app.route("/person", methods=["POST", "PUT"])
def add_person():
    with ensure_type(Person):
        person = load_request_json()
    session.add(person)
    session.commit()
    return "ok"

The above example is pretty contrived. We could have just made load_json_request accept an ensure_type kw, but imagine if the call to loader() was burried deeper in our api and such a thing was not possible.