Serializers and fields

The purpose of serializers and fields is to describe how structured is data that your API resources can return and accept. They together describe what we could call a “resource representation”.

They also helps binding this resource representation with internal objects that you use in your application no matter what you have there - dicts, native, class instances, ORM objects, documents, whatever. There is only one requirement: there must be a way to represent them as a set of independent fields and their values. In other words: dictionaries.

Example of simple serializer:

from graceful.serializers import BaseSerializer
from graceful.fields import RawField, IntField, FloatField


class CatSerializer(BaseSerializer):
    species = RawField("non normalized cat species")
    age = IntField("cat age in years")
    height = FloatField("cat height in cm")

Serializers are intended to be used with generic resources provided by graceful.resources.generic module so only handlers for retrieving, updating, creating etc. of objects from validated data is needed:

Functionally equivalent example using generic resources:

from graceful.resources.generic import RetrieveUpdateAPI
from graceful.serializers import BaseSerializer
from graceful.fields import RawField, FloatField

class Cat(object):
    def __init__(self, name, height):
        self.name = name
        self.height = height

class CatSerializer(BaseSerializer):
    name = RawField("name of a cat")
    height = FloatField("height in cm")

class CatResource(RetrieveUpdateAPI):
    serializer = CatSerializer()

    def retrieve(self, params, meta, **kwargs):
        return Cat('molly', 30)

    def update(self, params, meta, validated, **kwargs):
        return Cat(**validated)

Anyway serializers can be used outside of generic resources but some additional work need to be done then:

import falcon

from graceful.resources.base import BaseResource

class CatResource(BaseResource):
    serializer = CatSerializer()

    def on_get(self, req, resp, **kwargs):
        # this in probably should be read from storage
        cat = Cat('molly', 30)

        self.make_body(
            req, resp,
            meta={},
            content=self.serializer.to_representation(cat),
        )

    def on_put(self, req, resp, **kwargs)
        validated = self.require_validated(req)
        updated_cat = Cat(**validated)

        self.make_body(
            req, resp,
            meta={},
            # may be nothing or again representation of new cat
            content=self.serializer.to_representation(new_cat),
        )

        req.status = falcon.HTTP_CREATED

Field arguments

All field classes accept this set of arguments:

  • details (str, required): verbose description of field.

  • label (str, optional): human readable label for this field (it will be used for describing resource on OPTIONS requests).

    Note that it is recomended to use field names that are self-explanatory intead of relying on param labels.

  • source (str, optional): name of internal object key/attribute that will be passed to field’s on .to_representation(value) call. Special '*' value is allowed that will pass whole object to field when making representation. If not set then default source will be a field name used as a serializer’s attribute.

  • validators (list, optional): list of validator callables.

  • many (bool, optional): set to True if field is in fact a list of given type objects

  • read_only (bool): True if field is read-only and cannot be set/modified via POST, PUT, or PATCH requests.

  • write_only (bool): True if field is write-only and cannot be retrieved via GET requests.

Note

source='*' is in fact a dirty workaround and will not work well on validation when new object instances needs to be created/updated using POST/PUT requests. This works quite well with simple retrieve/list type resources but in more sophisticated cases it is better to use custom object properties as sources to encapsulate such fields.

Field validation

Additional validation of field value can be added to each field as a list of callables. Any callable that accepts single argument can be a validator but in order to provide correct HTTP responses each validator shoud raise graceful.errors.ValidationError exception on validation call.

Note

Concept of validation for fields is understood here as a process of checking if data of valid type (successfully parsed/processed by .from_representation handler) does meet some other constraints (lenght, bounds, unique, etc).

Example of simple validator usage:

from graceful.errors import ValidationError
from graceful.serializers import BaseSerializer
from graceful.fields import FloatField

def tiny_validator(value):
    if value > 20.0:
        raise ValidationError


class TinyCats(BaseSerializer):
    """ This resource accepts only cats that has height <= 20 cm """
    height = FloatField("cat height", validators=[tiny_validator])

graceful provides some small set of predefined validator helpers in graceful.validators module.

Resource validation

In most cases field level validation is all that you need but sometimes you need to perfom obejct level validation that needs to access multiple fields that are already deserialized and validated. Suggested way to do this in graceful is to override serializer’s .validate() method and raise graceful.errors.ValidationError when your validation fails. This exception will be then automatically translated to HTTP Bad Request response on resource-level handlers. Here is example:

class DrinkSerializer():
    alcohol = StringField("main ingredient", required=True)
    mixed_with = StringField("what makes it tasty", required=True)

    def validate(self, object_dict, partial=False):
        # note: always make sure to call super `validate()`
        # so whole validation of fields works as expected
        super().validate(object_dict, partial)

        # here is a place for your own validation
        if (
            object_dict['alcohol'] == 'whisky' and
            object_dict['mixed_with'] == 'cola'
        ):
            raise ValidationError("bartender refused!')

Custom fields

Custom field types can be created by subclassing of BaseField class and implementing of two method handlers:

  • .from_representation(raw): returns internal data type from raw string provided in request
  • .to_representation(data): returns representation of internal data type

Example of custom field that assumes that data in internal object is stored as a serialized JSON string that we would like to (de)serialize:

import json

from graceful.fields import BaseField


class JSONField(BaseField):
    def from_representation(raw):
        return json.dumps(raw)

    def to_representation(data):
        return json.loads(data)