Skip to content

Commit

Permalink
Changes to class-based full document serialization
Browse files Browse the repository at this point in the history
Serialization is now done using serializer classes. The class has a
`serialize()` and `serialize_many()`, each of which receives a full JSON
API document as a Python dictionary. Before, the serialization functions
received only the primary data.
  • Loading branch information
jfinkels committed Apr 4, 2016
1 parent 21315e4 commit c95587c
Show file tree
Hide file tree
Showing 14 changed files with 930 additions and 458 deletions.
142 changes: 87 additions & 55 deletions docs/customizing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,58 +127,78 @@ Bulk operations via the JSON API Bulk extension are not yet supported.
Custom serialization
~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 0.17.0

Flask-Restless provides serialization and deserialization that work with the
JSON API specification. If you wish to have more control over the way
instances of your models are converted to Python dictionary representations,
you can specify a custom serialization function by providing it to
:meth:`APIManager.create_api` via the ``serializer`` keyword argument.
Similarly, to provide a deserialization function that converts a Python
dictionary representation to an instance of your model, use the
``deserializer`` keyword argument. However, if you provide a serializer that
fails to produce resource objects that satisfy the JSON API specification, your
client will receive non-compliant responses!

Define your serialization functions like this::

def serialize(instance, only=None):
return {'id': ..., 'type': ..., 'attributes': ...}

``instance`` is an instance of a SQLAlchemy model and the ``only`` argument is
a list; only the fields (that is, the attributes and relationships) whose names
appear as strings in `only` should appear in the returned dictionary. The only
exception is that the keys ``'id'`` and ``'type'`` must always appear,
regardless of whether they appear in `only`. The function must return a
dictionary representation of the resource object.

To help with creating custom serialization functions, Flask-Restless provides a
:func:`simple_serialize` function, which returns the result of its basic,
built-in serialization. Therefore, one way to customize your serialized objects
is to do something like this::

from flask.ext.restless import simple_serialize

def my_serializer(instance, only=None):
# Get the default serialization of the instance.
result = simple_serialize(instance, only=only)
# Make your changes here.
result['meta']['foo'] = 'bar'
# Return the dictionary.
return result

You could also define a subclass of the :class:`DefaultSerializer` class,
override the :meth:`DefaultSerializer.__call__` method, and provide an instance
of that class to the `serializer` keyword argument.

For deserialization, define your custom deserialization function like this::

def deserialize(document):
return Person(...)
you can specify custom serialization by providing it to
:meth:`APIManager.create_api` via the ``serializer_class`` keyword argument.
Similarly, to provide a deserializer that converts a Python dictionary
representation to an instance of your model, use the ``deserializer_class``
keyword argument. However, if you provide a serializer that fails to produce
resource objects that satisfy the JSON API specification, your client will
receive non-compliant responses!

Your serializer classes must be a subclass of
:class:`~flask.ext.restless.Serializer` and can override the
:meth:`~flask.ext.restless.Serializer.serialize` and
:meth:`~flask.ext.restless.Serializer.serialize_many` methods to provide custom
serialization (however, we recommend overriding the
:class:`~flask.ext.restless.DefaultSerializer` class, as it provides some
useful behavior in its constructor). These methods take an instance or
instances as input and return a dictionary representing a JSON API
document. Each also accepts an ``only`` keyword argument, indicating the sparse
fieldsets requested by the client. When implementing your custom serializer,
you may wish to override the :class:`flask.ext.restless.DefaultSerializer`
class::

from flask.ext.restless import DefaultSerializer

class MySerializer(DefaultSerializer):

def serialize(self, instance, only=None):
super_serialize = super(DefaultSerializer, self).serialize
document = super_serialize(instance, only=only)
# Make changes to the document here...
...
return document

def serialize_many(self, instances, only=None):
super_serialize = super(DefaultSerializer, self).serialize_many
document = super_serialize(instances, only=only)
# Make changes to the document here...
...
return document

``instance`` is an instance of a SQLAlchemy model, ``instances`` is a list of
instances, and the ``only`` argument is a list; only the fields (that is, the
attributes and relationships) whose names appear as strings in `only` should
appear in the returned dictionary. The only exception is that the keys ``'id'``
and ``'type'`` must always appear, regardless of whether they appear in
`only`. The function must return a dictionary representation of the resource
object.

Flask-Restless also provides functional access to the default serialization,
via the :func:`~flask.ext.restless.simple_serialize` and
:func:`~flask.ext.restless.simple_serialize_many` functions, which return the
result of the built-in default serialization.

For deserialization, define your custom deserialization class like this::

from flask.ext.restless import DefaultDeserializer

class MyDeserializer(DefaultDeserializer):

def deserialize(self, document):
return Person(...)

def deserialize_many(self, document):
return Person(...)

``document`` is a dictionary representation of the *complete* incoming JSON API
document, where the ``data`` element contains the primary resource object. The
function must return an instance of the model that has the requested fields.
document, where the ``data`` element contains the primary resource object or
objects. The function must return an instance of the model that has the
requested fields. If you override the constructor, it must take two positional
arguments, `session` and `model`.

.. note::

Expand All @@ -197,21 +217,33 @@ follows::
name = fields.String()

def make_object(self, data):
print('MAKING OBJECT FROM', data)
return Person(**data)

person_schema = PersonSchema()
class PersonSerializer(DefaultSerializer):

def serialize(self, instance, only=None):
person_schema = PersonSchema(only=only)
return person_schema.dump(instance).data

def serialize_many(self, instances, only=None):
person_schema = PersonSchema(many=True, only=only)
return person_schema.dump(instances).data


class PersonDeserializer(DefaultDeserializer):

def person_serializer(instance):
return person_schema.dump(instance).data
def deserialize(self, document):
person_schema = PersonSchema()
return person_schema.load(instance).data

def person_deserializer(data):
return person_schema.load(data).data
def deserialize_many(self, document):
person_schema = PersonSchema(many=True)
return person_schema.load(instance).data

manager = APIManager(app, session=session)
manager.create_api(Person, methods=['GET', 'POST'],
serializer=person_serializer,
deserializer=person_deserializer)
serializer_class=PersonSerializer,
deserializer_class=PersonDeserializer)

For a complete version of this example, see the
:file:`examples/server_configurations/custom_serialization.py` module in the
Expand Down
42 changes: 30 additions & 12 deletions examples/server_configurations/custom_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,21 @@
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.restless import APIManager
from flask.ext.restless import DefaultSerializer
from flask.ext.restless import DefaultDeserializer
from marshmallow import Schema
from marshmallow import fields

app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)


class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode)
#birth_date = db.Column(db.Date)


class PersonSchema(Schema):
Expand All @@ -36,18 +38,34 @@ class PersonSchema(Schema):
def make_object(self, data):
return Person(**data)

person_schema = PersonSchema()

def person_serializer(instance):
return person_schema.dump(instance).data
class PersonSerializer(DefaultSerializer):

def person_deserializer(data):
return person_schema.load(data).data
def serialize(self, instance, only=None):
person_schema = PersonSchema(only=only)
return person_schema.dump(instance).data

db.create_all()
manager = APIManager(app, flask_sqlalchemy_db=db)
manager.create_api(Person, methods=['GET', 'POST'],
serializer=person_serializer,
deserializer=person_deserializer)
def serialize_many(self, instances, only=None):
person_schema = PersonSchema(many=True, only=only)
return person_schema.dump(instances).data

app.run()

class PersonDeserializer(DefaultDeserializer):

def deserialize(self, document):
person_schema = PersonSchema()
return person_schema.load(document).data

def deserialize_many(self, document):
person_schema = PersonSchema(many=True)
return person_schema.load(document).data


if __name__ == '__main__':
db.create_all()
manager = APIManager(app, flask_sqlalchemy_db=db)
manager.create_api(Person, methods=['GET', 'POST'],
serializer_class=PersonSerializer,
deserializer_class=PersonDeserializer)

app.run()
3 changes: 3 additions & 0 deletions flask_restless/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@
from .helpers import primary_key_for
from .manager import APIManager
from .manager import IllegalArgumentError
from .serialization import DefaultDeserializer
from .serialization import DefaultSerializer
from .serialization import DeserializationException
from .serialization import Deserializer
from .serialization import SerializationException
from .serialization import Serializer
from .serialization import simple_serialize
from .serialization import simple_serialize_many
from .views import CONTENT_TYPE
from .views import ProcessingException
26 changes: 20 additions & 6 deletions flask_restless/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,17 +204,31 @@ def primary_key_value(instance, as_string=False):
return url_quote_plus(result.encode('utf-8'))


def is_like_list(instance, relation):
"""Returns ``True`` if and only if the relation of `instance` whose name is
`relation` is list-like.
def is_like_list(model_or_instance, relation):
"""Returns ``True`` if and only if the relation of the given model or
instance of a model whose name is `relation` is list-like.
A relation may be like a list if, for example, it is a non-lazy one-to-many
relation, or it is a dynamically loaded one-to-many.
A relation may be like a list if, for example, it is a non-lazy
to-many relation, or it is a dynamically loaded to-many relation.
`model_or_instance` may be either a SQLAlchemy model class or an
instance of such a class.
"""
if inspect.isclass(model_or_instance):
model = model_or_instance
if relation in model._sa_class_manager:
return model._sa_class_manager[relation].property.uselist
related_value = getattr(model, relation)
if isinstance(related_value, AssociationProxy):
local_prop = related_value.local_attr.prop
if isinstance(local_prop, RelProperty):
return local_prop.uselist
return False
instance = model_or_instance
if relation in instance._sa_class_manager:
return instance._sa_class_manager[relation].property.uselist
elif hasattr(instance, relation):
if hasattr(instance, relation):
attr = getattr(instance._sa_instance_state.class_, relation)
if hasattr(attr, 'property'):
return attr.property.uselist
Expand Down
Loading

0 comments on commit c95587c

Please sign in to comment.