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.
- 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.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 itjsonweb
would 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_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 ahandler
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 ofObjectHook
and 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_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 byas_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 thehandlers
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 thehandler
andschema
values later. Here is an example of overriding a schema you defined withfrom_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
orhandlers
many 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 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__
inobj
will trigger a lookup inself.handlers
. If a handler is not found for__type__
then anObjectNotFoundError
is raised. If a handler is found it will be called withobj
as it only argument. If anObjectSchema
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
intoloader()
calls made in the active context. This will allow aValidationError
to 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_request
accept anensure_type
kw, but imagine if the call toloader()
was burried deeper in our api and such a thing was not possible.