Deserialize POST request with subset of serializer

2019-07-16 16:29发布

问题:

I'm having a rather simple problem, but found a few solutions and couldn't stop wondering what the intended DRF approach would be.

I have a (simplified) model and serializer like this:

class CartProduct(models.Model):
    cart = models.ForeignKey('Cart', on_delete=models.CASCADE)
    product = models.ForeignKey('Product', on_delete=models.CASCADE)

class CartProductSerializer(serializers.HyperlinkedModelSerializer):
    id = serializers.ReadOnlyField()
    product = ProductSerializer()

    class Meta:
        model = CartProduct
        fields = ('id', 'url', 'product')

Which produces a GET response like this:

"url": "http://localhost:8000/appUsers/1/cart/products/16/",
"id": 16,
"product": {
    "url": "http://localhost:8000/products/1/",
    "id": 1,
    "name": "Tomatoes",
},
"cart": "http://localhost:8000/carts/1/"

However, when creating a new CartProduct now, in this default scenario I would need to pass a nested product dictionary like the one above to create / deserialize a new CartProduct from a POST request.

What I would like instead is to send a POST request with a body using just primary keys or urls to create a new cart product, e.g. like this:

"product": 1,
"cart": 1

or

"product": "http://localhost:8000/products/1/"
"cart": "http://localhost:8000/carts/1/"

So now I was wondering what would be the best way to achieve this? I thought of:

  • Writing two separate serializers (but I don't like the idea of having two serializers for pretty much every model like this)
  • Adding additional fields to every serializer making sure that nested / related models are always represented by url and / or id and only making these ID fields required
  • Overriding the validation / create function in order to make the desired input a valid format
  • Overriding the ModelViewSet's create functions and dealing with the issue there

What would be the most appropriate place for dealing with such a case?

回答1:

I prefer to use the following approach where I have 2 serializer fields for 1 model field (one read only field for details and one id/url field for creates and updates):

class CartProductSerializer(serializers.HyperlinkedModelSerializer):
    product_detail = ProductSerializer(source='product', read_only=True) 

    class Meta:
        model = CartProduct
        fields = ('url', 'cart', 'product', 'product_detail')

Note that this assumes ProductSerializer is already defined elsewhere. And I'm omitting id because we don't really need it but you can still add it if you want.

This has the following advantages:

  • You can use the same serializer for all CRUD operations.
  • You get the nested field details on GET but can just provide ids for those nested fields on POST / PUT.
  • You don't have to write any custom logic in your views to parse etc. - you can stick with the default generic view functionality that you get out of the box

So in your specific case, the JSON you'll get back for a GET will be:

{
  "url": "http://localhost:8000/appUsers/1/cart/products/16/",
  "product": "http://localhost:8000/products/1/"
  "product_detail": {
    "url": "http://localhost:8000/products/1/",
    "name": "Tomatoes",
  },
  "cart": "http://localhost:8000/carts/1/"
}

And for POSTs you'll only need to send:

{
  "product": "http://localhost:8000/products/2/"
  "cart": "http://localhost:8000/carts/1/"
}

For PUTs, include the CartProduct object's own url in the above JSON.



回答2:

So you want the deserialized CartProductSerializer to include a nested representation of Product, while on the other hand, when serializing, you wish to provide only an id of an existing Product? You're right that creating an additional field is one solution, and I like it best.

  1. Set product as read-only, since you do not actually accept a nested product dictionary in your serializer (you can, though).
  2. Create a new field, product_id = ModelField(model_field=Product()._meta.get_field('id')). This will allow you to pass product_id when serializing. If you want to exclude this when deserializing, you can set it as write-only. See this answer.


回答3:

You can change serializer behaviour by overriding to_representation method

class CartProduct(models.Model):
    cart = models.ForeignKey('Cart', on_delete=models.CASCADE)
    product = models.ForeignKey('Product', on_delete=models.CASCADE)


class CartProductSerializer(serializers.HyperlinkedModelSerializer):
    id = serializers.ReadOnlyField()

    class Meta:
        model = CartProduct
        fields = ('id', 'url', 'product')

    def to_representation(self, instance):
        self.fields['product'] = ProductSerializer(read_only=True)
        return super().to_representation(instance)

This way your serializer will use PrimaryKeyRelatedField by default and on showing representation it will use nested ProductSerializer

NOTE: also you don't need to explicitly supply id field if it is default AutoField, just adding it in fields meta option should be enough