简体   繁体   中英

How to validate against full model data in Django REST Framework

TL;DR

In my Serializer.validate method, I need to be able to access attrs['field'] and fall back to self.instance.field if it's not set in the incoming data, and I'm wondering if there is a common pattern to do so.

The Problem

Take the example from the Object-level validation section of the DRF Serializers documentation:

from rest_framework import serializers

class EventSerializer(serializers.Serializer):
    description = serializers.CharField(max_length=100)
    start = serializers.DateTimeField()
    finish = serializers.DateTimeField()

    def validate(self, attrs):
        """
        Check that start is before finish.
        """
        if attrs['start'] > attrs['finish']:
            raise serializers.ValidationError("finish must occur after start")
        return attrs

(This uses a normal Serializer , but imagine it's a ModelSerializer for an Event model.)

When an event is created, or updated where both the start and finish attributes are included in the data, then this serializer works as expected.

However, if you make a request such as:

client.patch(f"/events/{event.id}", {"start": "2021-01-01"})

Then the serializer will fail, because trying to access attrs['finish'] results in a KeyError . In this case, I need to be able fall back to self.instance.finish , since the start < finish validation is still necessary.

Is there a common pattern that solves this problem?

Current Solution

You can solve this by adding a snippet to the start of all validate methods:

def validate(self, attrs):
    full_attrs = attrs
    if self.instance is not None:
        full_attrs = {
            **self.to_internal_value(self.__class__(self.instance).data),
            **full_attrs,
        }

Then use full_attrs in place of attrs . This adds the serialized version of the instance data to attrs .

Is there a better way to accomplish this?

(The one "downside" with this is that it could prevent otherwise valid updates if the data loses it's integrity. So for instance, if a developer updates the database directly so that event.start is later than event.finish , an API user will no longer be able to update event.description since this validation would fail. But I think the pros definitely outweigh the cons, at least for my current use case.)

I'll offer my take on this question because I have come across this problem in one of my projects.

I did the validation check in the model layer because:

  1. You no longer have to run the validation check on the serializer layer.
  2. The validation logic is closer to the database layer so you don't have to worry about "bad" data being created if someone decides to use django's ORM and create objects from the backend (eg import scripts).
  3. You validation logic sits closer to the code where the object is being created/saved so it's easier to debug

Validating it on the model layer is pretty simple. You can override the save method of the model class or the clean method and running the clean method (or full_clean ) in the save method. More details here .

from django.db import models
from django.core.exceptions import ValidationError


class MyModel(models.Model):
    start = ...
    finish = ...
    ...

    def clean(self):
        if self.finish < self.start:
            raise ValidationError("Finish must occur after start")

    def save(self, *args, **kwargs):
        self.full_clean()
        super().save(*args, **kwargs)

Now here's the thing about django's ValidationError. DRF doesn't know how to handle it. If you passed some invalid data to the serializer, you won't get a nice 400 response. To get DRF to handle the error, you write your own custom error handler and set it as the EXCEPTION_HANDLER in your settings.py .

# myapp/exceptions.py

from django.core.exceptions import ValidationError

from rest_framework.views import exception_handler
from rest_framework.response import Response


def django_error_handler(exc, context):
    """Handle django core's errors."""
    # Call REST framework's default exception handler first,
    # to get the standard error response.
    response = exception_handler(exc, context)
    if response is None and isinstance(exc, ValidationError):
        return Response(status=400, data=exc.message_dict)
    return response
# settings.py

REST_FRAMEWORK = {
    ...,
    'EXCEPTION_HANDLER': 'myapp.exceptions.django_error_handler'
}

Last Note

I notice you're using the generic serializers.Serializer class for your serializer. If you already have an Event model, it's easier to use serializers.ModelSerializer as this abstracts away the a lot of the object creation/update logic. Another benefit is that since it will look at your model's field definitions, it builds the fields according to how you have the fields specified in your model so you don't need to to define your fields in your serializer (eg If the field has a max_length , it'll create corresponding DRF field with a max length).

Rather additionally enforce integrity on database level by using CheckConstraint so you have no worries about inducing dirty data

class Meta:
        constraints = [
            models.CheckConstraint(
                check=models.Q(
                    finish > models.F("start")     
                    )
                ),
                name="start_gt_finish",
            )
        ]

To answer upon the choice of validation there is really no optimal solution. If your app is mix of ModelForms /Django admin and API it might be tempting to go with calling full_clean() but that might be also problematic as you would need to patch it somewhere as validate() does not call full_clean() nor properly converts all Django ValidationErrors .


I would say your solution seems fine and serves as serializer validation layer, and in case without database constraint you might check if start and stop are non existent in attrs to avoid validation check if patched data does not contain either start or stop

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM