Django-rest-framework: Source attribute for nested serializer doesn't work

Created on 7 Sep 2014  路  5Comments  路  Source: encode/django-rest-framework

Here is an example:

class ConfigSerializer(ApiSerializer):
   nodes = NodeSerializer(many=True, source='NODES')
>>> ConfigSerializer(data={'NODES':[{'name': ...}]}).errors
{'nodes': [u'This field is required.']}

Is it the expected behaviour or it's a bug?

Needs further review

Most helpful comment

@westandskif Ah, I think I figured out where it all went wrong :)

The source attribute on a field points to the model property. The documentation may be able to better explain it, but lets try an example.

Lets say we have these two models

class Foo(models.Model):
    something = models.TextField()
    bars = models.ManyToMany("app.Bar")

class Bar(models.Model):
    example = models.TextField()

And two matching serializers:

class BarSerializer(serializers.ModelSerializer):
    example = serializers.CharField()

class FooSerializer(serializers.ModelSerializer):
    something = serializers.CharField()
    children = BarSerializer(many=True, source="bars")

You'll notice on the Foo model, I have a field bars which is a many-to-many with Bar. Because this makes sense in the models, but doesn't in the serializer, I've essentially "aliased" it to Foo.children.

This should result in a serialized output similar to:

{
  "something": "Testing",
  "children": [
    {
      "example": "Something else"
    }
  ]
}

In the output, the alias of children is used. This is because the attribute name on the serializer was set as children, even though the source is still bars.

Now, if I wanted to send data back to the serializer to update it, I'd have to send it back as children as well. This enforces the idea that the input should match the output for full updates.

{
  "something": "Testing",
  "children": [
    {
      "example": "Updated field"
    }
  ]
}

This will pass all validation because all of the field names have been passed back. Now, if I decided to use bars instead of children, I'd get an error saying that children is a required field. This is important, because if it just used the data from bars then that would mean that the field has extra knowledge of the underlying data source - which it shouldn't.

I hope that cleared up any confusion I may have caused earlier.

All 5 comments

Unsure - assigned to 3.0 in order to review prior to incoming serializer redesign.

Is it the expected behaviour or it's a bug?

I believe this is expected behaviour. The source kwarg tells the field where the data is at (obj.NODES in this case), while the field name itself (nodes) is what DRF will use when serializing and deserializing the data.

When you are passing in the data to the serializer, you are using NODES which is the field source. Because field name are case-sensitive, DRF has no idea what to do with the data (and basically ignores it), which is why it is telling you that nodes is a required field - No data for it was passed in.

@kevin-brown I've really tried to do my best to understand your point, but I haven't managed to. Have no idea why DRF should ignore the data (because of field names are case-sensitive?). As I see, the source attribute points to the data source for every entry and that's why the following:

nodes = NodeSerializer(many=True, source='NODES')

should match {"NODES": [{node_obj}, ...]

@westandskif Ah, I think I figured out where it all went wrong :)

The source attribute on a field points to the model property. The documentation may be able to better explain it, but lets try an example.

Lets say we have these two models

class Foo(models.Model):
    something = models.TextField()
    bars = models.ManyToMany("app.Bar")

class Bar(models.Model):
    example = models.TextField()

And two matching serializers:

class BarSerializer(serializers.ModelSerializer):
    example = serializers.CharField()

class FooSerializer(serializers.ModelSerializer):
    something = serializers.CharField()
    children = BarSerializer(many=True, source="bars")

You'll notice on the Foo model, I have a field bars which is a many-to-many with Bar. Because this makes sense in the models, but doesn't in the serializer, I've essentially "aliased" it to Foo.children.

This should result in a serialized output similar to:

{
  "something": "Testing",
  "children": [
    {
      "example": "Something else"
    }
  ]
}

In the output, the alias of children is used. This is because the attribute name on the serializer was set as children, even though the source is still bars.

Now, if I wanted to send data back to the serializer to update it, I'd have to send it back as children as well. This enforces the idea that the input should match the output for full updates.

{
  "something": "Testing",
  "children": [
    {
      "example": "Updated field"
    }
  ]
}

This will pass all validation because all of the field names have been passed back. Now, if I decided to use bars instead of children, I'd get an error saying that children is a required field. This is important, because if it just used the data from bars then that would mean that the field has extra knowledge of the underlying data source - which it shouldn't.

I hope that cleared up any confusion I may have caused earlier.

@kevin-brown Thank you, man! Now it's perfectly clear for me! And thank you very much for time you've wasted to explain me it.

Was this page helpful?
0 / 5 - 0 ratings