Serializer relations
Relational fields are used to represent model relationships. They can be applied to foreign key,
many-to-many and one-to-one relationships, as well as to reverse relationships, and custom
relationships such as GenericForeignKey
of Django.
Note: The relational fields are declared in relations.py
, but by convention you should import
them from the serializers
module, using from aiorest-ws.db.orm.django import serializers
or
aiorest-ws.db.orm.sqlalchemy import serializers
and refer to fields as serializers.<FieldName>
.
Inspecting relationships
When using the ModelSerializer
class, serializer fields and relationships will be
automatically generated for you. Inspecting these automatically generated fields can be a useful
tool for determining how to customize the relationship style.
To do so, open the shell, then import the serializer class, instantiate it, and print the object
representation…
>>> from myapp.serializers import AccountSerializer
>>> serializer = AccountSerializer()
>>> print(repr(serializer))
AccountSerializer():
id = IntegerField(label='ID', read_only=True)
name = CharField(allow_blank=True, max_length=100, required=False)
owner = PrimaryKeyRelatedField(queryset=User.objects.all())
Serializer relation fields
In order to explain the various types of relational fields, we’ll use a couple of simple models
for our examples. Our models will be for music albums, and the tracks listed on each album, based
on the Django models.
class Album(models.Model):
album_name = models.CharField(max_length=100)
artist = models.CharField(max_length=100)
class Track(models.Model):
album = models.ForeignKey(Album, related_name='tracks', on_delete=models.CASCADE)
order = models.IntegerField()
title = models.CharField(max_length=100)
duration = models.IntegerField()
class Meta:
unique_together = ('album', 'order')
ordering = ['order']
def __unicode__(self):
return '%d: %s' % (self.order, self.title)
HyperlinkedIdentityField
This field can be applied as an identity relationship, such as the 'url'
field on a
HyperlinkedModelSerializer
. It can also be used for an attribute on the object. For
example, the following serializer:
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
track_listing = serializers.HyperlinkedIdentityField(view_name='track-list')
class Meta:
model = Album
fields = ('album_name', 'artist', 'track_listing')
Would serialize to a representation like this:
{
'album_name': 'The Eraser',
'artist': 'Thom Yorke',
'track_listing': 'http://www.example.com/api/track_list/12/',
}
This field is always read-only.
Arguments:
view_name
- The view name that should be used as the target of the relationship. If you’re
using the standard router classes this will be a string with the format <model_name>-detail
.
required.
lookup_field
- The field on the target that should be used for the lookup. Should correspond
to a URL keyword argument on the referenced view. Default is 'pk'
for Django or id
for
SQLAlchemy.
lookup_url_kwarg
- The name of the keyword argument defined in the URL conf that corresponds
to the lookup field. Defaults to using the same value as lookup_field
.
format
- If using format suffixes, hyperlinked fields will use the same format suffix for the
target unless overridden by using the format
argument.
Nested relationships
Nested relationships can be expressed by using serializers as fields.
If the field is used to represent a to-many relationship, you should add the many=True
flag to
the serializer field.
Example
For example, the following serializer:
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = ('order', 'title', 'duration')
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True, read_only=True)
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
Would serialize to a nested representation like this:
>>> album = Album.objects.create(album_name="The Grey Album", artist='Danger Mouse')
>>> Track.objects.create(album=album, order=1, title='Public Service Announcement', duration=245)
<Track: Track object>
>>> Track.objects.create(album=album, order=2, title='What More Can I Say', duration=264)
<Track: Track object>
>>> Track.objects.create(album=album, order=3, title='Encore', duration=159)
<Track: Track object>
>>> serializer = AlbumSerializer(instance=album)
>>> serializer.data
{
'album_name': 'The Grey Album',
'artist': 'Danger Mouse',
'tracks': [
{'order': 1, 'title': 'Public Service Announcement', 'duration': 245},
{'order': 2, 'title': 'What More Can I Say', 'duration': 264},
{'order': 3, 'title': 'Encore', 'duration': 159},
...
],
}
Writable nested serializers
By default nested serializers are read-only. If you want to support write-operations to a nested
serializer field you’ll need to create create()
and/or update()
methods in order to
explicitly specify how the child relationships should be saved.
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = ('order', 'title', 'duration')
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True)
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
def create(self, validated_data):
tracks_data = validated_data.pop('tracks')
album = Album.objects.create(**validated_data)
for track_data in tracks_data:
Track.objects.create(album=album, **track_data)
return album
>>> data = {
'album_name': 'The Grey Album',
'artist': 'Danger Mouse',
'tracks': [
{'order': 1, 'title': 'Public Service Announcement', 'duration': 245},
{'order': 2, 'title': 'What More Can I Say', 'duration': 264},
{'order': 3, 'title': 'Encore', 'duration': 159},
],
}
>>> serializer = AlbumSerializer(data=data)
>>> serializer.is_valid()
True
>>> serializer.save()
<Album: Album object>
Custom relational fields
In rare cases where none of the existing relational styles fit the representation you need,
you can implement a completely custom relational field, that describes exactly how the output
representation should be generated from the model instance.
To implement a custom relational field, you should override RelatedField
, and implement
the .to_representation(self, value)
method. This method takes the target of the field as the
value
argument, and should return the representation that should be used to serialize the target.
The value
argument will typically be a model instance.
If you want to implement a read-write relational field, you must also implement the
.to_internal_value(self, data)
method.
To provide a dynamic queryset based on the context
, you can also override .get_queryset(self)
instead of specifying .queryset
on the class or when initializing the field.
Example
For example, we could define a relational field to serialize a track to a custom string
representation, using its ordering, title, and duration.
import time
class TrackListingField(serializers.RelatedField):
def to_representation(self, value):
duration = time.strftime('%M:%S', time.gmtime(value.duration))
return 'Track %d: %s (%s)' % (value.order, value.name, duration)
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackListingField(many=True)
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
This custom field would then serialize to the following representation.
{
'album_name': 'Sometimes I Wish We Were an Eagle',
'artist': 'Bill Callahan',
'tracks': [
'Track 1: Jim Cain (04:39)',
'Track 2: Eid Ma Clack Shaw (04:19)',
'Track 3: The Wind and the Dove (04:34)',
...
]
}
Custom hyperlinked fields
In some cases you may need to customize the behavior of a hyperlinked field, in order to represent
URLs that require more than a single lookup field.
You can achieve this by overriding HyperlinkedRelatedField
. There are two methods that may
be overridden:
get_url(self, obj, view_name, request, format)
The get_url
method is used to map the object instance to its URL representation.
May raise a NoReverseMatch
if the view_name
and lookup_field
attributes are not
configured to correctly match the URL conf.
get_object(self, queryset, view_name, view_args, view_kwargs)
If you want to support a writable hyperlinked field then you’ll also want to override get_object
,
in order to map incoming URLs back to the object they represent. For read-only hyperlinked fields
there is no need to override this method.
The return value of this method should the object that corresponds to the matched URL conf arguments.
May raise an ObjectDoesNotExist
exception.
Example
Say we have a URL for a customer object that takes two keyword arguments, like so:
/api/<organization_slug>/customers/<customer_pk>/
This cannot be represented with the default implementation, which accepts only a single lookup
field.
In this case we’d need to override HyperlinkedRelatedField
to get the behavior we want:
from aiorest_ws.db.orm.django import serializers
from aiorest_ws.url.utils import reverse
class CustomerHyperlink(serializers.HyperlinkedRelatedField):
# We define these as class attributes, so we don't need to pass them as arguments.
view_name = 'customer-detail'
queryset = Customer.objects.all()
def get_url(self, obj, view_name, request, format):
relative = self.context.get('relative', False)
args = tuple(map(str, [obj.organization.slug, obj.pk]))
return reverse(view_name, args=args, relative=relative)
def get_object(self, view_name, view_args, view_kwargs):
lookup_kwargs = {
'organization__slug': view_kwargs['organization_slug'],
'pk': view_kwargs['customer_pk']
}
return self.get_queryset().get(**lookup_kwargs)
Note that if you wanted to use this style together with the generic views then you’d also need to
override .get_object
on the view in order to get the correct lookup behavior.
Generally we recommend a flat style for API representations where possible, but the nested URL
style can also be reasonable when used in moderation.