Serializers

Serializers which will be described below give to a programmer opportunities to convert complex data structures like model instances of certain ORM to the default Python datatypes. The result of this processing can be returned to a user in JSON, XML or any other formats. These serializers also provide deserializing mechanisms which can be used for parsing and validating user input data that will be used for to work with ORMs further.

These serializers largely based on ideas and concepts of Django REST Framework. In this way a lot of functionality that will be described further will coincide with this library.

At the moment aiorest-ws have support with the following ORMs:

  • Django
  • SQLAlchemy

You can find corresponding modules for each of mentioned ORMs in aiorest_ws.db.orm.* namespace.

Model serializers

The aiorest-ws library provides ModelSerializer classes which can be used for serializing (or deserializing) your model instances to a some specific format. Its can be used for processing of model instances which have taken from a database.

Defining model serializer

Let’s suppose that we have a some class which has certain functionality. For example it could be a class that storing data about a registered user:

from django.db import models

class User(models.Model):
    self.username = models.CharField(max_length=255, unique=True)
    self.email = models.EmailField()
    self.logged_at = models.DateTimeField(auto_now=True)

After this we will declare a serializer class which is used for serializing and deserializing some data that converted to User objects:

from aiorest_ws.db.orm.django import serializers

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User

That’s all! We have described our simplest serializer class that giving opportunities to work with User instances. As you can see there, we have declared Meta class, that is storing a link to the User model. When we will put some data to the serializer instance (or otherwise, update data), serializer will parse specified model and extract fields that will be processed.

Serializing

Serialization mechanism let us to convert complex types into Python language types. So for it is enough pass an existing object into a serializer instance and get data attribute after creating serializer. For example:

user = User.objects.create(username='nayton', email='nayton@example.com')
serializer = UserSerializer(user)
serializer.data
# {'pk': 1, 'username': 'nayton', 'email': 'nayton@example.com', 'logged_at': '2016-11-29T21:13:31.039488'}

As you can see, we have converted passed object into dictionary. So it now remains to make an additional step, that allow to transmit the data through a network. For instance we can render it into JSON:

from aiorest_ws.renderers import JSONRenderer

json = JSONRenderer().render(serializer.data)
json
# b'{"pk": 1, "username": "nayton", "email": "nayton@example.com", "logged_at": "2016-11-29T21:13:31.039488"}'

Deserializing

Deserializing data is very useful feature when you want to get information after users action or from 3rd party APIs and save it in a database as some model instances. For using this feature enough to use already declared serializer class:

data = {"username": "new_user", "email": "new_user@example.com", "logged_at": "2016-11-29T21:15:31.078217"}
serializer = UserSerializer(data=data)
serializer.is_valid()
# True
serializer.validated_data
# {'username': 'new_user', 'email': 'new_user@example.com', 'logged_at': datetime.datetime(2016, 11, 29, 21, 15, 31, 78217)}

Saving instances

In some cases if you want to return created object instances which based on the validated data then you will need to implement .create() and/or .update() methods. For example with Django ORM it can be looks like:

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User

    def create(self, validated_data):
        return User.objects.create(**validated_data)

    def update(self, instance, validated_data):
        instance.username = validated_data.get('username', instance.username)
        instance.email = validated_data.get('email', instance.email)
        instance.logged_at = validated_data.get('logged_at', instance.logged_at)
        instance.save()
        return instance

After then we implemented .save() method, it will return an object instance, based on the validated data:

user = serializer.save()

Worth noting that instances of ModelSerializer class can accept instance of a model as the an argument which is can be updated further. It leads to two different calls which can be used:

# The first case: `.save()` will create a new instance.
serializer = UserSerializer(data=data)

# The second case: `.save()` will update the existing instance.
serializer = UserSerializer(user, data=data)

Custom implementation for .create and .update() is optional. Override it only when it necessary for your use cases. You can implement one, both or neither of them for your own model serializer.

Passing additional attributes to .save()

Sometimes you can caught in the situation when necessary to pass additional data to the .save() method. These additional data can be represented like timestamps, session ids or anything else that is not part of the validated data. For solving this issue you can just include additional keyword arguments when calling .save() method. For example:

serializer.save(session_id=request.data['session_id'])

Each additional keyword arguments will be included in the serializer.validated_data argument when .create() or .update() are called.

Overriding .save() directly

In some cases you doesn’t want call the .create() or .update() methods, because in some use case you want to do some useful work instead of them.

The clear example for it is a feedback form which is filled by a user on your website. After a moment when you have taken the form, you are going to send a message to your administration team from a server with some text instead of saving or updating model instance. For example it might look like this:

class FeedbackMessageSerializer(serializers.ModelSerializer):

    class Meta:
        model = FeedbackMessage

    def save(self):
        email = self.validated_data['email']
        subject = self.validated_data['subject']
        message = self.validated_data['message']
        send_email(from=email, subject=subject, message=message)

Keep in mind that in this case we have a direct access to the serializer .validated_data attribute.

Validating

When deserializing data, you always should to call .is_valid() method. This method will validate passed data to a serializer instance and will return True value when the passed data are correct. Otherwise will be returned False value and all occurred errors during the validation process will be available through .errors property where each element representing the resulting error messages. For example:

data = {"username": "nayton", "email": "string", "logged_at": "2016-11-30T14:43:12.174129"}
serializer = UserSerializer(data=data)
serializer.is_valid()
# False
serializer.errors
# {'username': ['User already exists.'], 'email': ['Enter a valid e-mail address.']}

Raising an exception on invalid data

Our .is_valid() method also can get an optional raise_exception flag that allow to raise a aiorest_ws.db.orm.exceptions.ValidationError exception when returned any validation errors. For example:

serializer.is_valid(raise_exception=True)

Field-level validation

Sometimes you might need to make additional checks for a certain field during the validation process. You can apply this checks by adding .validate_<field_name>() to your ModelSerializer subclass. Each custom field-level validation method should return the validated value or raise a ValidationError. For example:

from aiorest_ws.db.orm.django import serializers
from aiorest_ws.db.orm.exceptions import ValidationError

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User

    def validate_username(self, value):
        if value.lower() == "admin":
            raise ValidationError("Username cannot be set to 'admin'.")
        return value

Note

If <field_name> is declared in your serializer with parameter required=False then this validation method will not apply if this field is not included.

Object-level validation

For a case when necessary to implement complex validation where are using multiple fields, you will need to implement .validate() method in your ModelSerialize subclass. This method takes a single argument which is represented as a dictionary of field values. As well this method also should return validated values or raise а ValidationError exception. For example:

from aiorest_ws.db.orm.django import serializers
from aiorest_ws.db.orm.exceptions import ValidationError

class MovieSessionSerializer(serializers.Serializer):
    name = serializers.CharField(max_length=50)
    description = serializers.CharField(max_length=255)
    start = serializers.DateTimeField()
    end = serializers.DateTimeField()

    class Meta:
        model = MovieSession

    def validate(self, data):
        """
        Check that the start is before the end.
        """
        if data['start'] > data['end']:
            raise ValidationError("Start timestamp cannot be greater than end.")
        return data

Validators

For each certain field can be applied list of validators via declaring them on the field instance. Those validators can be represented as functions or instances of some class. Each of them takes one argument during the validation, for example:

from aiorest_ws.db.orm.django import serializers
from aiorest_ws.db.orm.exceptions import ValidationError
from django.core.validators import MaxLengthValidator

def empty_string_validator(value):
    if not value.strip():
        raise ValidationError('Title cannot be empty.')

class PageSerializer(serializers.ModelSerializer):
    title = serializers.CharField(validators=[empty_string_validator, MaxLengthValidator(50)])
    ...

Also you can apply similar checks to the complete set of field data. For using this mechanism will be enough to specify validators attribute (as a list of used validators) in inner Meta class, like here:

from aiorest_ws.db.orm.django import serializers
from aiorest_ws.db.orm.validators import BaseUniqueFieldValidator

class UniqueTogetherValidator(BaseUniqueFieldValidator):
    """
    Special validator class that check on uniqueness pair of fields.
    """
    def __init__(self, queryset, fields, message=None):
        super(UniqueTogetherValidator, self).__init__(queryset, message)
        self.fields = fields

    def __call__(self, attrs):
        ...

class TrackSerializer(serializers.ModelSerializer):
    name = serializers.CharField(max_length=100)
    duration = serializers.TimeField()
    track_number = serializers.IntegerField()
    cd_name = serializers.CharField(max_length=255)

    class Meta:
        model = Track
        validators = [
            UniqueTogetherValidator(
                queryset=Track.objects.all(),
                fields=('track_number', 'cd_name')
            ),
        ]

Accessing the initial data and instance

After creating an instance of a ModelSerializer subclass you can get access to following attributes which can be useful during validation or creating/updating objects: .instance and .initial_data.

The first one, .instance attribute, can be set as the first argument of aserializer subclass or with “instance” keyword. The passed instance can be initial object or queryset. If no initial object is passed then the .instance attribute will be None.

The second one, .initial_data attribute, can be set as the second argument of a serializer subclass or with “data” keyword. And when you are passing data to a serializer instance, the unmodified data will be made available as .initial_data. If the data keyword argument is not passed then the .initial_data attribute will not exist.

Partial instance updates

By default for each serializer you must specify all required fields or it will raise validation errors. For using partial updates you will need to pass partial flag with True value. For example:

# Update `user` with partial data
serializer = UserSerializer(user, data={'email': 'new_nayton_email@exampl.com'}, partial=True)

Dealing with nested objects

The above examples are pretty fine demonstrating how to work with a models that have a simple datatypes. But in most projects with which you will be work, will be have models this relations with which also necessary to work. And you expecting that will be able to represent more complex objects, that contains not only default datatypes.

Because each field class and ModelSerializer subclass have the same parent AbstractSerializer class, you can use these model serializers for represent relationships where one object type is nested inside another.

Note

And here we have a difference with Django REST framework. The author of aiorest-ws library have divided the Field class onto two different classes: model serializer and its model fields. This is done in order to make it easier for programmers to understand what is happening in the process of debugging.

from aiorest_ws.db.orm.django import serializers

class CategorySerializer(serializers.ModelSerializer):
    name = serializers.CharField(max_length=255)

    class Meta:
        model = Category

class PostSerializer(serializers.Serializer):
    category = CategorySerializer()
    title = serializers.CharField(max_length=255)
    content = serializers.CharField(max_length=3000)

    class Meta:
        model = Post

In the case if a nested serializer may accept the None value, you should pass the required=False value to this nested serializer:

class PostSerializer(serializers.Serializer):
    category = CategorySerializer(required=False)  # Post can be without a category
    title = serializers.CharField(max_length=255)
    content = serializers.CharField(max_length=3000)

    class Meta:
        model = Post

For a case when a nested serializer should be represented as a list of objects, specify the many=True value to the nested serializer:

class PostSerializer(serializers.Serializer):
    category = CategorySerializer(many=True)  # A list of categories
    title = serializers.CharField(max_length=255)
    content = serializers.CharField(max_length=3000)

    class Meta:
        model = Post

Writable nested representations

When dealing with nested representations that support deserializing the data, any errors with nested objects will be nested under the field name of the nested object.

data = {'category': {'name': ''}, 'title': 'aiorest-ws docs', 'content': 'The first version of docs.'}
serializer = PostSerializer(data=data)
serializer.is_valid()
# False
serializer.errors
# {'category': {'name': ['This field may not be blank.']}}

Similarly, the .validated_data property will include nested data structures.

Writing .create() methods for nested representations

For a case when necessary to support writable nested representations for a model serializer, you will need to override .create() or .update() methods that giving opportunities to work with multiple objects.

Take a look on the next example that demonstrate you how to create a Post instance with nested objects:

class PostSerializer(serializers.Serializer):
    category = CategorySerializer(many=True)
    title = serializers.CharField(max_length=255)
    content = serializers.CharField(max_length=3000)

    class Meta:
        model = Post
        fields = ('category', 'title', 'content')

    def create(self, validated_data):
        category_data = validated_data.pop('category')
        post = Post.objects.create(**validated_data)
        Category.objects.create(post=post, **category_data)
        return post

Writing .update() methods for nested representations

For a case when necessary to update an instance with nested relationships is more complex. First of all you must decide how to handle updates to relationships. What to do if the data for model relationship is None value, or perhaps not provided? We can use one of the following solutions:

  • Set the relationship to NULL in the database.
  • Delete the associated instance.
  • Ignore the data and leave the instance as it is.
  • Raise a validation error.

Take a look on the next example for an .update() method of PostSerializer class:

def update(self, instance, validated_data):
       category_data = validated_data.pop('category')
       # Unless the application properly enforces that this field is
       # always set, the follow could raise a `DoesNotExist`, which
       # would need to be handled.
       category_instance = instance.category
       category_instance.name = category_data.get('name', 'Blog')
       category_instance.save()

       instance.title = validated_data.get('title', instance.title)
       instance.content = validated_data.get('content', instance.content)
       instance.save()

       return instance

Because the behavior of nested creates and updates can be ambiguous, and may require complex dependencies between related models, ModelSerializer requires you to always write these methods explicitly. The default ModelSerializer .create() and .update() methods do not include support for writable nested representations.

Dealing with multiple objects

For each supported ORM the ModelSerializer class can also handle serializing or deserializing lists of objects.

Serializing multiple objects

For a case when necessary to serialize a queryset or list of objects instead of a single object instance, just specify the many=True flag when instantiating the model serializer. You can then specify a queryset or list of objects to be serialized.

queryset = Category.objects.all()
serializer = CategorySerializer(queryset, many=True)
serializer.data
# [
#     {'name': 'Documentation'},
#     {'name': 'Features'},
#     {'name': 'Change notes'}
# ]

Deserializing multiple objects

The default behavior for deserializing multiple objects is to support multiple object creation, but not support multiple object updates. For more information on how to support or customize either of these cases, see the ListSerializer documentation below.

Including extra context

In some cases you may need to provide extra context which can be used by serializer during the validating or in addition to the object being serialized. You can do this by passing a context argument when instantiating the serializer. For example:

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('username', 'email')

    def to_representation(self, instance):
        data = super(UserSerializer, self).to_representation(instance)
        data['request_id'] = self.context['request'].request_id
        return data

serializer = UserSerializer(user, context={'request': request})
serializer.data
# {"username": "nayton", "email": "nayton@example.com", "request_id": "76c3d654-b804-11e6-a794-0c4de9c846b0"}

The context dictionary can be used within any serializer field logic, such as a custom .to_representation() method, by accessing the self.context attribute.

ModelSerializer

All information that described above applied for this class and its subclasses. The ModelSerializer class define an interface that help developers in serializing and deserializing models of Django and SQLAlchemy ORMs.

What can ModelSerializer class:

  • Automatically extract and generate a set of fields based on the specified model.
  • Apply validators (and user-defined also) for the serializer and its fields.
  • Provides implementations for .create() and .update() method by default.

Example of using:

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('pk', 'username', 'email', 'logged_at')

Inspecting a ModelSerializer

Model serializers provide a very helpful representation strings, that allow you to fully inspect a state for each serializer field. This is very useful during debugging and let you understand what kind of fields and validators are being automatically created for you.

To do so, open the you application shell, after that import the model serializer class, instantiate it, and print the object representation:

>>> from app.serializers import CarSerializer
>>> serializer = CarSerializer()
>>> print(repr(serializer))
... CarSerializer():
...    id = IntegerField(label=ID, read_only=True)
...    name = CharField(max_length=30, validators=['<UniqueValidator(queryset=django_orm_example.Car.objects)>'])
...    manufacturer = PrimaryKeyRelatedField(label=Manufacturer, queryset=django_orm_example.Manufacturer.objects)

Specifying which fields to include

If you only want a subset of the default fields to be used in a model serializer, you can do so using fields or exclude options, just as you would with a ModelForm in Django. It is strongly recommended that you explicitly set all fields that should be serialized using the fields attribute. This will make it less likely to result in unintentionally exposing data when your models change.

For example:

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('username', 'email', 'logged_at')

You can also set the fields attribute to the special value __all__ to indicate that all fields in the model should be used.

For example:

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = '__all__'

You can set the exclude attribute to a list of fields to be excluded from the serializer.

For example:

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        exclude = ('logged_at', )

In the example above, if the User model had 3 fields username, email, and logged_at, this will result in the fields username and email to be serialized.

The names in the fields and exclude attributes will normally map to model fields on the model class.

Alternatively names in the fields options can map to properties or methods which take no arguments that exist on the model class.

Specifying nested serialization

The default ModelSerializer uses primary keys for relationships, but you can also easily generate nested representations using the depth option:

class CarSerializer(serializers.ModelSerializer):

    class Meta:
        model = Account
        fields = ('id', 'name', 'manufacturer')
        depth = 1

The depth option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation.

If you want to customize the way the serialization is done you’ll need to define the field yourself.

Specifying fields explicitly

You can add extra fields to a ModelSerializer or override the default fields by declaring fields on the class.

class CarSerializer(serializers.ModelSerializer):
    name = serializers.CharField(read_only=True)
    manufacturer = serializers.PrimaryKeyRelatedField()

    class Meta:
        model = Car

Extra fields can correspond to any property or callable on the model.

Specifying read only fields

You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the read_only=True attribute, you may use the shortcut Meta option, read_only_fields.

This option should be a list or tuple of field names, and is declared as follows:

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('username', 'email', 'logged_at')
        read_only_fields = ('email', )

Model fields which have editable=False set, and AutoField fields will be set to read-only by default, and do not need to be added to the read_only_fields option.

Additional keyword arguments

There is also a shortcut allowing you to specify arbitrary additional keyword arguments on fields, using the extra_kwargs option. As in the case of read_only_fields, this means you do not need to explicitly declare the field on the serializer.

This option is a dictionary, mapping field names to a dictionary of keyword arguments. For example:

class CreateUserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('email', 'username', 'password')
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = User(
            email=validated_data['email'],
            username=validated_data['username']
        )
        user.set_password(validated_data['password'])
        user.save()
        return user

Relational fields

When serializing model instances, there are a number of different ways you might choose to represent relationships. The default representation for ModelSerializer is to use the primary keys of the related instances.

Alternative representations include serializing using hyperlinks, serializing complete nested representations, or serializing with a custom representation.

Inheritance of the ‘Meta’ class

The inner Meta class on serializers is not inherited from parent classes by default. This is the same behavior as with Django’s model. If you want the Meta class to inherit from a parent class you must do so explicitly. For example:

class UserSerializer(MyBaseSerializer):

    class Meta(MyBaseSerializer.Meta):
        model = User

Typically we would recommend not using inheritance on inner Meta classes, but instead declaring all options explicitly.

Customizing field mappings

The ModelSerializer class also exposes an API that you can override in order to alter how serializer fields are automatically determined when instantiating the serializer.

Normally if a ModelSerializer does not generate the fields you need by default then you should either add them to the class explicitly. However in some cases you may want to create a new base class that defines how the serializer fields are created for any given model.

  • .serializer_field_mapping

A mapping of Django or SQLAlchemy model classes to aiorest-ws serializer classes. You can override this mapping to alter the default serializer classes that should be used for each model class.

  • .serializer_related_field

This property should be the serializer field class, that is used for relational fields by default.

For ModelSerializer this defaults to PrimaryKeyRelatedField`.

For HyperlinkedModelSerializer this defaults to serializers.HyperlinkedRelatedField.

  • serializer_url_field

The serializer field class that should be used for any url field on the serializer.

Defaults to serializers.HyperlinkedIdentityField

  • serializer_choice_field

The serializer field class that should be used for any choice fields on the serializer.

Defaults to serializers.ChoiceField for Django ORM and serializers.EnumField for SQLAlchemy ORM.

The field_class and field_kwargs API

The following methods are called to determine the class and keyword arguments for each field that should be automatically included on the serializer. Each of these methods should return a two tuple of (field_class, field_kwargs).

  • .build_standard_field(self, field_name, model_field)

Called to generate a serializer field that maps to a standard model field.

The default implementation returns a serializer class based on the serializer_field_mapping attribute.

  • .build_relational_field(self, field_name, relation_info)

Called to generate a serializer field that maps to a relational model field.

The default implementation returns a serializer class based on the serializer_relational_field attribute.

The relation_info argument is a named tuple, that contains model_field, related_model, to_many and has_through_model properties.

  • .build_nested_field(self, field_name, relation_info, nested_depth)

Called to generate a serializer field that maps to a relational model field, when the depth option has been set.

The default implementation dynamically creates a nested serializer class based on either ModelSerializer or HyperlinkedModelSerializer.

The nested_depth will be the value of the depth option, minus one.

The relation_info argument is a named tuple, that contains model_field, related_model, to_many and has_through_model properties.

  • .build_property_field(self, field_name, model_class)

Called to generate a serializer field that maps to a property or zero-argument method on the model class.

The default implementation returns a ReadOnlyField class.

  • .build_url_field(self, field_name, model_class)

Called to generate a serializer field for the serializer’s own url field. The default implementation returns a HyperlinkedIdentityField class.

  • .build_unknown_field(self, field_name, model_class)

Called when the field name did not map to any model field or model property. The default implementation raises an error, although subclasses may customize this behavior.

HyperlinkedModelSerializer

The HyperlinkedModelSerializer class is similar to the ModelSerializer class except that it uses hyperlinks to represent relationships, rather than primary keys.

By default the serializer will include a url field instead of a primary key field.

The url field will be represented using a HyperlinkedIdentityField serializer field, and any relationships on the model will be represented using a HyperlinkedRelatedField serializer field.

You can explicitly include the primary key by adding it to the fields option, for example:

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('url', 'username', 'email', 'logged_at')

Absolute and relative URLs

When instantiating a HyperlinkedModelSerializer you do not need to include the current request in the serializer context as it implemented in Django REST. Just create an serialized instance and call the .data attribute:

serializer = UserSerializer(user_queryset)

So, after getting a data from serializer instance, you will get absolute path to this object instead of primary key:

"wss://127.0.0.1:8000/user/1/"

If you do want to use relative URLs, you should explicitly pass {'relative': True} to the serializer constructor as the context argument:

serializer = UserSerializer(user_object, context={'relative': True})

After that you will get a relative link to an object:

"/user/1/"

How hyperlinked views are determined

There needs to be a way of determining which views should be used for hyperlinking to model instances.

By default hyperlinks are expected to correspond to a view name that matches the style '{model_name}-detail', and looks up the instance by a pk keyword argument.

You can override a URL field view name and lookup field by using either, or both of, the view_name and lookup_field options in the extra_kwargs setting, like so:

class CarSerializer(serializers.HyperlinkedModelSerializer):

    class Meta:
        model = Account
        fields = ('car_url', 'car_model', 'factories')
        extra_kwargs = {
            'url': {'view_name': 'cars', 'lookup_field': 'car_model'}
            'factories': {'lookup_field': 'name'}
        }

Alternatively you can set the fields on the serializer explicitly. For example:

class CarSerializer(serializers.HyperlinkedModelSerializer):
    url = serializers.HyperlinkedIdentityField(
        view_name='cars', lookup_field='car_model'
    )
    factories = serializers.HyperlinkedRelatedField(
        view_name='factory-detail', lookup_field='name',
        many=True, read_only=True
    )

    class Meta:
        model = Account
        fields = ('url', 'car_model', 'factories')

Tip: Properly matching together hyperlinked representations and your URL conf can sometimes be a bit fiddly. Printing the repr of a HyperlinkedModelSerializer instance is a particularly useful way to inspect exactly which view names and lookup fields the relationships are expected to map too.

Changing the URL field name

The name of the URL field defaults to url. You can override this globally, by using the URL_FIELD_NAME setting.

ListSerializer

The ListSerializer class provides the behavior for serializing and validating multiple objects at once. You won’t typically need to use ListSerializer directly, but should instead simply pass many=True when instantiating a serializer.

When a serializer is instantiated and many=True is passed, a ListSerializer instance will be created. The serializer class then becomes a child of the parent ListSerializer.

The following argument can also be passed to a ListSerializer field or a serializer that is passed many=True:

allow_empty

This is True by default, but can be set to False if you want to disallow empty lists as valid input.

Customizing ListSerializer behavior

There are a few use cases when you might want to customize the ListSerializer behavior. For example:

  • You want to provide particular validation of the lists, such as checking that one element does not conflict with another element in a list.
  • You want to customize the create or update behavior of multiple objects.

For these cases you can modify the class that is used when many=True is passed, by using the list_serializer_class option on the serializer Meta class.

For example:

class CustomListSerializer(serializers.ListSerializer):
    ...

class CustomSerializer(serializers.Serializer):
    ...
    class Meta:
        list_serializer_class = CustomListSerializer

Customizing multiple create

The default implementation for multiple object creation is to simply call .create() for each item in the list. If you want to customize this behavior, you’ll need to customize the .create() method on ListSerializer class that is used when many=True is passed.

For example:

class BookListSerializer(serializers.ListSerializer):

    def create(self, validated_data):
        books = [Book(**item) for item in validated_data]
        return Book.objects.bulk_create(books)

class BookSerializer(serializers.Serializer):
    ...
    class Meta:
        list_serializer_class = BookListSerializer

Customizing multiple update

By default the ListSerializer class does not support multiple updates. This is because the behavior that should be expected for insertions and deletions is ambiguous.

To support multiple updates you’ll need to do so explicitly. When writing your multiple update code make sure to keep the following in mind:

  • How do you determine which instance should be updated for each item in the list of data?
  • How should insertions be handled? Are they invalid, or do they create new objects?
  • How should removals be handled? Do they imply object deletion, or removing a relationship? Should they be silently ignored, or are they invalid?
  • How should ordering be handled? Does changing the position of two items imply any state change or is it ignored?

You will need to add an explicit id field to the instance serializer. The default implicitly-generated id field is marked as read_only. This causes it to be removed on updates. Once you declare it explicitly, it will be available in the list serializer’s update method.

Here’s an example of how you might choose to implement multiple updates:

class BookListSerializer(serializers.ListSerializer):

    def update(self, instance, validated_data):
        # Maps for id->instance and id->data item.
        book_mapping = {book.id: book for book in instance}
        data_mapping = {item['id']: item for item in validated_data}

        # Perform creations and updates.
        ret = []
        for book_id, data in data_mapping.items():
            book = book_mapping.get(book_id, None)
            if book is None:
                ret.append(self.child.create(data))
            else:
                ret.append(self.child.update(book, data))

        # Perform deletions.
        for book_id, book in book_mapping.items():
            if book_id not in data_mapping:
                book.delete()

        return ret

class BookSerializer(serializers.Serializer):
    # We need to identify elements in the list using their primary key,
    # so use a writable field here, rather than the default which would be read-only.
    id = serializers.IntegerField()

    ...
    id = serializers.IntegerField(required=False)

    class Meta:
        list_serializer_class = BookListSerializer

Customizing ListSerializer initialization

When a serializer with many=True is instantiated, we need to determine which arguments and keyword arguments should be passed to the .__init__() method for both the child Serializer class, and for the parent ListSerializer class.

The default implementation is to pass all arguments to both classes, except for validators, and any custom keyword arguments, both of which are assumed to be intended for the child serializer class.

Occasionally you might need to explicitly specify how the child and parent classes should be instantiated when many=True is passed. You can do so by using the many_init class method.

@classmethod
def many_init(cls, *args, **kwargs):
    # Instantiate the child serializer.
    kwargs['child'] = cls()
    # Instantiate the parent list serializer.
    return CustomListSerializer(*args, **kwargs)