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.
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?
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:
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'
}
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.