Django REST framework flat, read-write serializer

2019-02-01 11:44发布

问题:

In Django REST framework, what is involved in creating a flat, read-write serializer representation? The docs refer to a 'flat representation' (end of the section http://django-rest-framework.org/api-guide/serializers.html#dealing-with-nested-objects) but don't offer examples or anything beyond a suggestion to use a RelatedField subclass.

For instance, how to provide a flat representation of the User and UserProfile relationship, below?

# Model
class UserProfile(models.Model):
    user = models.OneToOneField(User)
    favourite_number = models.IntegerField()

# Serializer
class UserProfileSerializer(serializers.ModelSerializer):
    email = serialisers.EmailField(source='user.email')
    class Meta:
        model = UserProfile
        fields = ['id', 'favourite_number', 'email',]

The above UserProfileSerializer doesn't allow writing to the email field, but I hope it expresses the intention sufficiently well. So, how should a 'flat' read-write serializer be constructed to allow a writable email attribute on the UserProfileSerializer? Is it at all possible to do this when subclassing ModelSerializer?

Thanks.

回答1:

Looking at the Django REST framework (DRF) source I settled on the view that a DRF serializer is strongly tied to an accompanying Model for unserializing purposes. Field's source param make this less so for serializing purposes.

With that in mind, and viewing serializers as encapsulating validation and save behaviour (in addition to their (un)serializing behaviour) I used two serializers: one for each of the User and UserProfile models:

class UserSerializer(serializer.ModelSerializer):
    class Meta:
        model = User
        fields = ['email',]

class UserProfileSerializer(serializer.ModelSerializer):
    email = serializers.EmailField(source='user.email')
    class Meta:
        model = UserProfile
        fields = ['id', 'favourite_number', 'email',]

The source param on the EmailField handles the serialization case adequately (e.g. when servicing GET requests). For unserializing (e.g. when serivicing PUT requests) it is necessary to do a little work in the view, combining the validation and save behaviour of the two serializers:

class UserProfileRetrieveUpdate(generics.GenericAPIView):
    def get(self, request, *args, **kwargs):
        # Only UserProfileSerializer is required to serialize data since
        # email is populated by the 'source' param on EmailField.
        serializer = UserProfileSerializer(
                instance=request.user.get_profile())
        return Response(serializer.data)

    def put(self, request, *args, **kwargs):
        # Both UserProfileSerializer and UserProfileSerializer are required
        # in order to validate and save data on their associated models.
        user_profile_serializer = UserProfileSerializer(
                instance=request.user.get_profile(),
                data=request.DATA)
        user_serializer = UserSerializer(
                instance=request.user,
                data=request.DATA)
        if user_profile_serializer.is_valid() and user_serializer.is_valid():
            user_profile_serializer.save()
            user_serializer.save()
            return Response(
                    user_profile_serializer.data, status=status.HTTP_200_OK)
        # Combine errors from both serializers.
        errors = dict()
        errors.update(user_profile_serializer.errors)
        errors.update(user_serializer.errors)
        return Response(errors, status=status.HTTP_400_BAD_REQUEST)


回答2:

First: better handling of nested writes is on it's way.

Second: The Serializer Relations docs say of both PrimaryKeyRelatedField and SlugRelatedField that "By default this field is read-write..." — so if your email field was unique (is it?) it might be you could use the SlugRelatedField and it would just work — I've not tried this yet (however).

Third: Instead I've used a plain Field subclass that uses the source="*" technique to accept the whole object. From there I manually pull the related field in to_native and return that — this is read-only. In order to write I've checked request.DATA in post_save and updated the related object there — This isn't automatic but it works.

So, Fourth: Looking at what you've already got, my approach (above) amounts to marking your email field as read-only and then implementing post_save to check for an email value and perform the update accordingly.



回答3:

Although this does not strictly answer the question - I think it will solve your need. The issue may be more in the split of two models to represent one entity than an issue with DRF.

Since Django 1.5, you can make a custom user, if all you want is some method and extra fields but apart from that you are happy with the Django user, then all you need to do is:

class MyUser(AbstractBaseUser): favourite_number = models.IntegerField()

and in settings: AUTH_USER_MODEL = 'myapp.myuser'

(And of course a db-migration, which could be made quite simple by using db_table option to point to your existing user table and just add the new columns there).

After that, you have the common case which DRF excels at.