Skip to content

Add ListSerializer.get_child hook #5847

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from

Conversation

rpkilby
Copy link
Member

@rpkilby rpkilby commented Feb 23, 2018

This should resolve #5537 and similar issues by allowing users to customize/override the ListSerializer child on a per-instance/per-item basis. Currently, the child is created once during initialization, and customizing the child requires duplicating a few methods, such as to_internal_value.

As a practical example, I have a polymorphic serializer class that needs to uses different child instances to validate the various types.

@tomchristie
Copy link
Member

Want to give an example of usage here? Then we can figure out if we'd also need to make this public API, and where we'd update the docs if so.

@rpkilby
Copy link
Member Author

rpkilby commented Feb 23, 2018

Sure - just to expand on my particular case, I have a polymorphic serializer that can represent multiple models that inherit a common parent. As a contrived example, take a bunch of physical media types that we want to catalog.

class Media(models.Model):
    collection = models.ForeignKey(...)

class Article(Media):
    author = models.ForeignKey(...)

class Book(Media)
    authors = models.ManytoManyField(...)

class Photograph(Media):
    photographer = models.ForeignKey(...)

I've created a polymorphic serializer class that can represent all of the sub types as part of the same collection. Example setup:

class MediaSerializer(PolymorphicModelSerializer):
    class Meta:
        model = models.Media
        types = OrderedDict([
            (models.Article, ArticleSerializer),
            (models.Book, BookSerializer),
            (models.Photo, PhotoSerializer),
        ])
        list_serializer_class = PolymorphicListSerializer
>>> collection = Collection.objects.create(...)
>>> article = Article.objects.create(collection=collection, author=...)
>>> book = Book.objects.create(collection=collection, authors=[...])
>>> photograph = Photograph.objects.create(collection=collection, photographer=...)

>>> serializer = MediaSerializer(instance=collection.media.all(), many=True)
>>> serializer.data

The resulting json would look something like:

[
    {
        "type": "article",
        "id": 1,
        "collection": 1,
        "author": "..."
    }, {
        "type": "book",
        "id": 2,
        "collection": 1,
        "author": ["..."]
    }, {
        "type": "photograph",
        "id": 3,
        "collection": 1,
        "photographer": "..."
    }
]

This also supports creating/updating multiple objects.


Anyway, this all works right now, but as explained above, it's necessary to duplicate methods like to_internal_serializer, which I'd rather avoid.

@tomchristie
Copy link
Member

Seems reasonable. What does the implementation of PolynorphicListSerializer look like assuming we pulled this in?

@rpkilby
Copy link
Member Author

rpkilby commented Feb 23, 2018

I wasn't looking to merge the polymorphic serializer (at least not any time soon), but I could try and whip up a PR at some point. Right now the code is somewhat specific to the project and makes a few assumptions, which should probably be cleaned up:

  • Hardcoded assumption that the type field will be called type
  • Expects that the parent model uses the InheritanceManager from django-model-utils

@tomchristie
Copy link
Member

I wasn't looking to merge the polymorphic serializer

I know - I was more looking for an example of using the new hook.

@rpkilby
Copy link
Member Author

rpkilby commented Feb 23, 2018

Oh, sure. Code is included in the collapsed details below.

Expand

polymorphic serializer:

from django.db import models
from rest_framework import serializers


def verbose_name(model):
    opts = model._meta

    # if model._deferred:
    #     return verbose_name(opts.proxy_for_model)
    return opts.verbose_name.lower()


class CheckedTypeField(serializers.Field):

    def __init__(self, **kwargs):
        # The `source` needs to be set to '*', since there is no corresponding
        # 'type' field on the model. The type is instead derived via the model's
        # meta class. This is relevant to serialization of model instances.
        kwargs['source'] = '*'
        super(CheckedTypeField, self).__init__(**kwargs)

    def run_validation(self, data):
        self.validate_type(data)
        return super(CheckedTypeField, self).run_validation(data)

    def validate_type(self, data):
        expected = verbose_name(self.parent.Meta.model)
        if data != expected:
            raise serializers.ValidationError(
                'Invalid type. Expected \'{}\', but got \'{}\'.'.format(
                    expected, data,
                )
            )

    def to_internal_value(self, data):
        return {}

    def to_representation(self, value):
        return verbose_name(value)


class CheckedPolymorphicTypeField(CheckedTypeField):

    def validate_type(self, data):
        expected = [verbose_name(model) for model in self.parent.Meta.types]
        if data not in expected:
            raise serializers.ValidationError(
                'Invalid type. Expected one of {}, but got \'{}\'.'.format(
                    expected, data,
                )
            )


class PolymorphicModelSerializer(serializers.ModelSerializer):
    type = CheckedPolymorphicTypeField()

    def __new__(cls, instance=None, data=serializers.empty, **kwargs):
        assert hasattr(cls.Meta, 'types')

        # get the serializer subclass for provided data
        if isinstance(data, dict):
            model = {
                verbose_name(model): model for model in cls.Meta.types
            }.get(data.get('type'))

            if model is not None:
                cls = cls.Meta.types[model]
                return cls(instance=instance, data=data, **kwargs)

        # get the serializer subclass for an instance
        if hasattr(instance, '_meta'):
            model = instance._meta.model

            if model in cls.Meta.types:
                cls = cls.Meta.types[model]
                return cls(instance=instance, data=data, **kwargs)

        return super(PolymorphicModelSerializer, cls) \
            .__new__(cls, instance=instance, data=data, **kwargs)


class PolymorphicListSerializer(serializers.ListSerializer):

    def __init__(self, instance=None, data=serializers.empty, **kwargs):
        self.child_class = type(kwargs['child'])
        self.child_kwargs = kwargs.copy()
        self.child_kwargs.pop('child')

        super().__init__(instance=instance, data=data, **kwargs)

    def get_child(self, instance=None, data=serializers.empty):
        return self.child_class(instance=instance, data=data,
                                **self.child_kwargs)

    def get_initial(self):
        if hasattr(self, 'initial_data'):
            return [
                self.get_child(data=item).get_initial()
                for item in self.initial_data
            ]
        return []

    def create(self, validated_data):
        return [
            self.get_child(data=data).create(attrs)
            for data, attrs in zip(self.get_initial(), validated_data)
        ]

example usage:

class ArticleSerializer(serializers.ModelSerializer):
    type = CheckedTypeField()

    class Meta:
        model = models.Article
        fields = ['id', 'type', 'author', 'title', 'content']

class MediaSerializer(PolymorphicModelSerializer):
    class Meta:
        model = models.Media
        types = OrderedDict([
            (models.Article, ArticleSerializer),
            (models.Book, BookSerializer),
            (models.Photo, PhotoSerializer),
        ])
        fields = ['type']
        list_serializer_class = PolymorphicListSerializer

@tomchristie
Copy link
Member

I'd like to close this off. Agree that we don't do a great job of handling polymorphic cases, but I don't really want us to make the serializers any more complicated than they already are.

@tomchristie tomchristie closed this Apr 4, 2018
@rpkilby rpkilby deleted the list-serializer-child branch June 23, 2018 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants