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_obj, loader
>>> @from_object()
... class Person(object):
... def __init__(self, first_name, last_name):
... self.first_name = first_name
... self.last_name = last_name
>>> person_json = '{"__type__": "Person", "first_name": "Shawn", "last_name": "Adams"}'
>>> person = loader(person_json)
>>> 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.
- ensure_type – Check that the resulting object is of type
Decorators¶
-
jsonweb.decode.from_object(handler=None, type_name=None, schema=None)¶ Decorating a class with
from_object()will allowjson.loads()to return instances of that class.handleris a callable that should return your class instance. It receives two arguments, your class and a python dict. Here is an example:>>> import json >>> 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 itjsonwebwould not know which handler to delegate the python dict to. By defaultfrom_object()assumes__type__will be the class’s__name__attribute. You can specify your own value by setting thetype_namekeyword argument:@from_object(person_decoder, "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>>> from josnweb.decode import ObjectNotFoundError >>> luke = loader('{"__type__": "Jedi", "name": "Luke"}') Traceback (most recent call last): ... ObjectNotFoundError: Cannot decode object Jedi. No such object.
You may have noticed that
handleris optional. If you do not specify ahandlerjsonwebwill 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
*argsand**kwargs. The handler would not know which keys to pull out of the python dict.Lets look at a few examples:
>>> @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
genderoptional it must be a keyword argument:>>> @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
schemakeyword agrument. Here is a quick example:>>> from jsonweb.schema import ObjectSchema, ValidationError >>> from jsonweb.schema.validators import 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 webservices or other web applications. For a detailed explination 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 ofObjectHookand return a callable suitable for passing tojson.loads()asobject_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 useas_typeto 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 byas_type.handlersis 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 thehandlerskeyword argument. Here is an exmaple:>>> 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 thehandlerandschemavalues later. Here is an example of overriding a schema you defined withfrom_object()(some code is left out for brevity):>>> @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_typeorhandlersmany times in your code you can forgo usingloader()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 instiating and using it.-
decode_obj(obj)¶ This method is called for every dict decoded in a json string. The presense of the key
__type__inobjwill trigger a lookup inself.handlers. If a handler is not found for__type__then anObjectNotFoundErroris raised. If a handler is found it will be called withobjas it only argument. If anObjectSchemawas supplied for the class,objwill 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_typeintoloader()calls made in the active context. This will allow aValidationErrorto bubble up from the underlyingloader()call if the resultant type is not of typeensure_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_requestaccept anensure_typekw, but imagine if the call toloader()was burried deeper in our api and such a thing was not possible.