DRF Writable Nested

repository·master·Indexed 22 days ago

https://github.com/beda-software/drf-writable-nested

A Django REST Framework extension that enables writable nested model serializers. It allows the creation and update of complex object graphs in a single API call, supporting OneToOne, ForeignKey, ManyToMany, and GenericRelation relationship types. The library provides the WritableNestedModelSerializer class and UniqueFieldsMixin to handle unique field validation during the save stage.

Tokens
2.5K
Snippets
7
Records
8
Agent score
28%

What's inside drf-writable-nested

  1. Supported Django relations in drf-writable-nested

    master

    The library supports the following relationship types for nested creation and updates:

    • OneToOne: Both direct and reverse relations.
    • ForeignKey: Both direct and reverse relations.
    • ManyToMany: Both direct and reverse relations (excluding M2M relations that use a through model).
    • GenericRelation: Only reverse relations.
  2. Use WritableNestedModelSerializer for nested data

    master

    To allow creating or updating models with related nested data, inherit from WritableNestedModelSerializer. You must define the nested serializers for the related fields within your main serializer.

    Example of defining a nested structure:

    from drf_writable_nested.serializers import WritableNestedModelSerializer
    
    class ProfileSerializer(WritableNestedModelSerializer):
        # Direct ManyToMany relation
        sites = SiteSerializer(many=True)
    
        # Reverse FK relation
        avatars = AvatarSerializer(many=True)
    
        # Direct FK relation
        access_key = AccessKeySerializer(allow_null=True)
    
        class Meta:
            model = Profile
            fields = ('pk', 'sites', 'avatars', 'access_key',)
    from rest_framework import serializers
    from drf_writable_nested.serializers import WritableNestedModelSerializer
    
    class AvatarSerializer(serializers.ModelSerializer):
        image = serializers.CharField()
    
        class Meta:
            model = Avatar
            fields = ('pk', 'image',)
    
    
    class SiteSerializer(serializers.ModelSerializer):
        url = serializers.CharField()
    
        class Meta:
            model = Site
            fields = ('pk', 'url',)
    
    
    class AccessKeySerializer(serializers.ModelSerializer):
    
        class Meta:
            model = AccessKey
            fields = ('pk', 'key',)
    
    
    class ProfileSerializer(WritableNestedModelSerializer):
        # Direct ManyToMany relation
        sites = SiteSerializer(many=True)
    
        # Reverse FK relation
        avatars = AvatarSerializer(many=True)
    
        # Direct FK relation
        access_key = AccessKeySerializer(allow_null=True)
    
        class Meta:
            model = Profile
            fields = ('pk', 'sites', 'avatars', 'access_key',)
    
    
    class UserSerializer(WritableNestedModelSerializer):
        # Reverse OneToOne relation
        profile = ProfileSerializer()
    
        class Meta:
            model = User
            fields = ('pk', 'profile', 'username',)
  3. Run tests for drf-writable-nested

    master

    To run the unit tests, follow these steps to set up a virtual environment and execute pytest:

    # Setup the virtual environment
    python3 -m venv envname
    source envname/bin/activate
    
    pip install django
    pip install django-rest-framework
    pip install -r requirements.txt
    
    # Run tests
    py.test
  4. Handle unique fields in nested serializers with UniqueFieldsMixin

    master

    When updating nested serializers that contain unique fields, standard DRF validation may fail. Use UniqueFieldsMixin to move UniqueValidator from the validation stage to the save stage.

    Important Rules:

    1. Apply UniqueFieldsMixin only to the serializer that contains the unique fields.
    2. When using multiple mixins, UniqueFieldsMixin must be placed ahead of NestedCreateMixin or NestedUpdateMixin in the inheritance list.
    class ChildSerializer(UniqueFieldsMixin, NestedUpdateMixin, serializers.ModelSerializer):
        class Meta:
            model = Child
    
    class ParentSerializer(NestedUpdateMixin, serializers.ModelSerializer):
        child = ChildSerializer()
    
        class Meta:
            model = Parent
    class Child(models.Model):
        field = models.CharField(unique=True)
    
    
    class Parent(models.Model):
        child = models.ForeignKey('Child')
    
    
    class ChildSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
        class Meta:
            model = Child
    
    
    class ParentSerializer(NestedUpdateMixin, serializers.ModelSerializer):
        child = ChildSerializer()
    
        class Meta:
            model = Parent
  5. Workaround for nested fields in form-data (PUT/PATCH)

    master

    Updating nested fields via form-data in PUT or PATCH requests is currently unsupported by DRF and this library (e.g., using voucherrows[1].account=... syntax).

    Solution: Instead of sending array fields as separate form-data keys, send the entire nested array as a single JSON string. Then, in your view, parse that JSON string back into a Python object before passing it to the serializer.

    Example Implementation:

    import json
    
    class VoucherViewSet(viewsets.ModelViewSet):
        serializer_class = VoucherSerializer
        queryset = serializer_class.Meta.model.objects.all().order_by('-created_at')
        
        def update(self, request, *args, **kwargs):
            # Parse the 'voucherrows' field from a JSON string into a list/dict
            request.data.update({
                'voucherrows': json.loads(request.data.pop('voucherrows', None))
            })
            return super().update(request, *args, **kwargs)
    # From your views you need to parse it like below before sending it to the serializer
    class VoucherViewSet(viewsets.ModelViewSet):
        serializer_class = VoucherSerializer
        queryset = serializer_class.Meta.model.objects.all().order_by('-created_at')
        
        def update(self, request, *args, **kwargs):
            request.data.update({'voucherrows': json.loads(request.data.pop('voucherrows', None))})
            return super().update(request, *args, **kwargs)
  6. Create and update nested models with data

    master

    Once your WritableNestedModelSerializer is defined, you can pass nested dictionaries or lists in the data argument to .is_valid() and .save(). The serializer will automatically handle the creation/update of all nested relations.

    data = {
        'username': 'test',
        'profile': {
            'access_key': {
                'key': 'key',
            },
            'sites': [
                {'url': 'http://google.com'},
                {'url': 'http://yahoo.com'},
            ],
            'avatars': [
                {'image': 'image-1.png'},
                {'image': 'image-2.png'},
            ],
        },
    }
    
    user_serializer = UserSerializer(data=data)
    user_serializer.is_valid(raise_exception=True)
    user = user_serializer.save()
    data = {
        'username': 'test',
        'profile': {
            'access_key': {
                'key': 'key',
            },
            'sites': [
                {
                    'url': 'http://google.com',
                },
                {
                    'url': 'http://yahoo.com',
                },
            ],
            'avatars': [
                {
                    'image': 'image-1.png',
                },
                {
                    'image': 'image-2.png',
                },
            ],
        },
    }
    
    user_serializer = UserSerializer(data=data)
    user_serializer.is_valid(raise_exception=True)
    user = user_serializer.save()
  7. Pass kwargs to nested serializers during save()

    master

    You can pass a dictionary of values to the base serializer's save() method. These values will be passed down to the nested serializers. Note that the same value will be used for all nested instances as a default, but with higher priority than existing defaults.

    # user_serializer created with 'data' as above
    user = user_serializer.save(
        profile={
            'access_key': {'key': 'key2'},
        },
    )
    print(user.profile.access_key.key) # Output: 'key2'
    # user_serializer created with 'data' as above
    user = user_serializer.save(
        profile={
            'access_key': {'key': 'key2'},
        },
    )
    print(user.profile.access_key.key)