Serializers

Serializers are used to take complex python models and translate them into json. Serializers can also be used to deserialize json back to the python models after validating the incoming data.

At Sentry we have two different types of serializers: Django Rest Framework serializers and model serializers.

Django Rest Framework

Django Rest Framework's serializers are used to handle input validation and transformation for data coming into Sentry.

Example

In the typical serializer, the fields are specified so that they validate the type and format of the data to your specifications. Django Rest Framework serializers can also save the information into the database if written to fit to the model.

Copied
from rest_framework import serializers
from sentry.api.serializers.rest_framework import ValidationError

class ExampleSerializer(serializers.Serializer):
    name = serializers.CharField()
    age = serializers.IntegerField(required=False)
    type = serializers.CharField()

    def validate_type(self, attrs, source):
        type = attrs[source]
        if type not in ['bear', 'rabbit', 'puppy']:
            raise ValidationError('%s is not a valid type' % type)
	return attrs

Field Checking

In the above example the serializer will accept and validate json containing three fields: name, age, and type. Where name and type must be strings and age must be an integer as suggested. By default, fields are required, and if not supplied will be marked as invalid by the serializer. Note that the integer field age, required is set to False. And so may not be included and the serializer would still be considered valid.

Custom Validation

For values that need custom validation (in addition to simple type checking), a

def validate_<variable_name>(self, attrs, source)

can be created where <variable_name> is substituted with the exact variable name as the field is given. So for example if I had a field name typeName the validate method name would be validate_typeName whereas if I had a field named type_name the validate method name would be validate_type_name. In the example given above, type is checked an must be a certain string. If a field does not match what your validate method is expecting raise a ValidationError.

Usage

In an endpoint, this is the typical use of a Django Rest Framework Serializer

Copied
class ExampleEndpoint(Endpoint):
    def post(self, request):
        serializer = ExampleSerializer(request.DATA)
        if not serializer.is_valid():
            return Response(serializer.errors, status=400)

        result = serializer.object

        #Assuming Example is a model with the same fields 
        try:
            with transaction.atomic():
                Example.objects.create(
                    name=result['name'],
                    age=result.get('age'),
                    type=result['type'],
                )
        except IntegrityError:
            return Response('This example already exists', status=409)

        return Response(serialize(result, request.user), status=201)

Validating Data

The Serializer from the Django Rest Framework will be used in methods with incoming data (i.e. put and post methods) that need to be validated. Once the serializer is instantiated, you can call serializer.is_valid() to validate the data. serializer.errors will give feedback on specifically what was invalid about the data given.

For example given input

Copied
{
	'age':5,
	'type':'puppy'
}

The serializer would return an error stating that the required field name was not provided.

Saving Data

Once you have verified that the data is valid, you can save the data in one of two ways. The example given above is the most commonly done in sentry. Taking the serializer.object which is simply the validated data (and will be None if serializer.is_valid() return False) and saving that data directly in the model with <ModelName>.objects.create.

An alternative method uses more of Django Rest Framework's features, the ModelSerializer

Copied
from rest_framework import serializers
from sentry.api.serializers.rest_framework import ValidationError

class ExampleSerializer(serializer.ModelSerializer):
    name = serializers.CharField()
    age = serializers.IntegerField(required=False)
    type = serializers.CharField()

    class Meta:
        model = Example
 
    def validate_type(self, attrs, source):
        type = attrs[source]
        if type not in ['bear', 'rabbit', 'puppy']:
            raise ValidationError('%s is not a valid type' % type)
        return attrs

class ExampleEndpoint(Endpoint):
    def post(self, request):
        serializer = ExampleSerializer(request.DATA)
        if not serializer.is_valid():
            return Response(serializer.errors, status=400)

        example = serializer.save()
        return Response(serialize(example, request.user), status=201)

Model Serializer

Sentry's Model Serializers are a home grown version that is used only for outgoing data. The typical model serializer looks like this:

Copied
@register(Example)
class ExampleSerializer(Serializer):
    def get_attrs(self, item_list, user):
        attrs = {}
        types = ExampleTypes.objects.filter(
            type_name__in=[i.type for i in item_list]
        )

        for item in item_list:
            attrs[item] = {}
            attrs[item]['type'] = [t for t in types if t.name == item.type_name]
	    return attrs

    def serialize(self, obj, attrs, user):
        return {
            'name':obj.name,
            'type':attrs['type'],
            'age': obj.age,
        }

Registering Model Serializers

The decorator @register is required so that

Copied
return Response(serialize(example, request.user), status=201)

works. Under the hood it searches for a matching model Example in this case, given the type of model the variable example is. To match the model serializer with the Model you simply do

Copied
@register(<ModelName>)
class ModelSerializer(Serializer):
...

get_attrs Method

Why do this when Django Rest Framework has similar functionality? The get_attrs method is the reason. It allows you to do a bulk query versus multiple queries. In our example, instead of calling ExampleTypes.objects.get(...) multiple items, I can filter for the ones I want and assign them to the item in question using python. In the case of attr dictionary, the key is the item iteself. and the value is a dictionary with the name of the attribute you want to add and it's values.

Copied
attrs[item] = {'attribute_name': attribute}

Serialize Method

Finally you return a dictionary with json serializable information that will be returned with the response.

You can edit this page on GitHub.