Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
199 views
in Technique[技术] by (71.8m points)

python - 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.)

question from:https://stackoverflow.com/questions/65912181/how-to-validate-against-full-model-data-in-django-rest-framework

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 (e.g. 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 (e.g. If the field has a max_length, it'll create corresponding DRF field with a max length).


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...