FlyingZipper
FlyingZipper

Reputation: 711

Django RF ErrorDetail : This field is required with APIClient

I'm trying to create an object with a serializer from a post request but I'm getting an error while trying to pass a list of objects to a nested serializer. While passing the ('id', 'name', 'description') data in a JSON format works just fine the list of bars is not getting parsed properly and return the following error :

{'bars' : [ErrorDetail(string='This field is required.', code='required')]}}

Those are the Serializers :

class BarsSerializer(serializers.ModelSerializer):
    
    class Meta:
        model = Bar
        fields = ('name', 'foo')


class FooSerializer(serializers.ModelSerializer):

    bars = BarsSerializer(many=True)
    
    class Meta:
        model = Foo
        fields = ('id', 'author' 'name', 'description', 'bars')

    def create(self, validated_data):
        validated_data['author'] = self.context['request'].user
        # Foo object manager is tested and works
        return Foo.objects.create(**validated_data)

This is my request payload :

{
    'name': "A Foo",
    'description': "A happy foo running in the woods of Central Park",
    'bars':  [
        {name : 'a'},
        {name : 'b'},
        {name : 'c'},
    ]
}

Those are the models

class Bar(models.Model):
    
    name = models.CharField(
        max_length=255,
        default=""
    )


    foo = models.ForeignKey(
        Foo,
        related_name='foos',
        on_delete=models.CASCADE
    )

class Foo(models.Model):
    
    name = models.CharField(
            max_length=255
        )

    description = models.CharField(
            max_length=1023,
            default=""
        )

    author = models.ForeignKey(CommonUser, on_delete=models.SET_NULL, null=True)

Update

the problem is only there while testing with Django python manage.py test and not there while testing the request with postman with the local server

data = {
    "name": "A Foo",
    "description": "A happy foo running in the woods of Central Park",
    "bars":  [
        {name : "a"},
        {name : "b"},
        {name : "c"},
    ]
}

res = self.client_one.post(reverse('foo-list'), data)

note that Foo and Bar are both simplified model of my real models to reduce the information amount of the given problem

Upvotes: 3

Views: 1895

Answers (2)

FlyingZipper
FlyingZipper

Reputation: 711

While testing the models with the APIClient rest_framework.test.APIClient and making a post request with more complex data and nested serializer, the format must be set explicitly to json as such

def test_a_feature(self):
    self.client = APIClient()

    payload = {
        "name": "A Foo",
        "description": "A happy foo running in the woods of Central Park",
        "bars":  [
            {name : "a"},
            {name : "b"},
            {name : "c"},
        ]
    }

    self.client.post(reverse('foo-list'), payload, format='json')

Upvotes: 3

Rayyan
Rayyan

Reputation: 302

I think I have found a great solution to your issue in the docs

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:

The docs continue to give an example of how you would implement this:

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>

So in your case, you would define the FooSerializer and the BarsSerializer like this:

class BarsSerializer(serializers.ModelSerializer):
    class Meta:
         model = Bar
         fields = ['name'] # Note that I have removed the 'foo' field since I have re-written your create method in the FooSerializer.


class FooSerializer(serializers.ModelSerializer):
     bars = BarsSerializer(many=True)
     class Meta:
          model = Foo
          fields = ['id', 'author', 'name', 'description', 'bars']

     def create(self, validated_data):
        validated_data['author'] = self.context['request'].user
        bars_data = validated_data.pop('bars') # Validated data is your dict with the 'bars' list nested inside it
        Foo = Foo.objects.create(**validated_data)  # Foo object manager is tested and works


        # Now we just iterate over the bars list...
        for bar_data in bars_data:
             #... and create a bar for each bar_data
             Bar.objects.create(foo=foo, **bar_data)

        return Foo

I haven't tested this code and it may not work, but it is taken right from the docs.

Upvotes: 0

Related Questions