Use pydantic with Django REST framework
Pydantic is a Python library used to perform data serialization and validation.
Django REST framework is a framework built on top of Django used to write REST APIs.
If you develop DRF APIs and rely on pydantic for data validation/(de)serialization,
then drf-pydantic
is for you 😍.
Note
The latest version of drf_pydantic
only supports pydantic
v2.
Support for pydantic
v1 is available in the 1.*
version.
Translation between pydantic
models and DRF
serializers is done during class
creation (e.g., when you first import the model). This means there will be
zero runtime impact when using drf_pydantic
in your application.
Note
There will be a minor penalty if validate_pydantic
is set to True
due to pydantic
model validation. This is minimal compared to an-already present overhead of DRF
itself because pydantic runs its validation in rust while DRF is pure python.
pip install drf-pydantic
Use drf_pydantic.BaseModel
instead of pydantic.BaseModel
when creating your models:
from drf_pydantic import BaseModel
class MyModel(BaseModel):
name: str
addresses: list[str]
MyModel.drf_serializer
is equivalent to the following DRF Serializer class:
class MyModelSerializer:
name = CharField(allow_null=False, required=True)
addresses = ListField(
allow_empty=True,
allow_null=False,
child=CharField(allow_null=False),
required=True,
)
Whenever you need a DRF serializer, you can get it from the model like this:
my_value = MyModel.drf_serializer(data={"name": "Van", "addresses": ["Gym"]})
my_value.is_valid(raise_exception=True)
Note
Models created using drf_pydantic
are fully identical to those created by
pydantic
. The only change is the addition of the drf_serializer
and drf_config
attributes.
By default, the generated serializer only uses DRF's validation; however, pydantic
models are often more complex and their numerous validation rules cannot be fully
translated to DRF. To enable pydantic validators to run whenever the generated
DRF serializer validates its data (e.g., via .is_valid()
),
set "validate_pydantic": True
within the drf_config
property of your model:
from drf_pydantic import BaseModel
class MyModel(BaseModel):
name: str
addresses: list[str]
drf_config = {"validate_pydantic": True}
my_serializer = MyModel.drf_serializer(data={"name": "Van", "addresses": []})
my_serializer.is_valid() # this will also validate MyModel
With this option enabled, every time you validate data using your DRF serializer,
the parent pydantic model is also validated. If it fails, its
ValidationError
exception will be wrapped within DRF's ValidationError.
Per-field and non-field (object-level) errors are wrapped
similarly to how DRF handles them. This ensures your complex pydantic validation logic
is properly evaluated wherever a DRF serializer is used.
Note
All drf_config
values are properly inherited by child classes,
just like pydantic's model_config
.
By default, drf_pydantic
updates values in the DRF serializer
with those from the validated pydantic model:
from drf_pydantic import BaseModel
class MyModel(BaseModel):
name: str
addresses: list[str]
@pydantic.field_validator("name")
@classmethod
def validate_name(cls, v):
assert isinstance(v, str)
return v.strip().title()
drf_config = {"validate_pydantic": True}
my_serializer = MyModel.drf_serializer(data={"name": "van herrington", "addresses": []})
my_serializer.is_valid()
print(my_serializer.data) # {"name": "Van Herrington", "addresses": []}
This is handy when you dynamically modify field values within your
pydantic validators. You can disable this behavior by setting
"backpopulate_after_validation": False
:
class MyModel(BaseModel):
...
drf_config = {"validate_pydantic": True, "backpopulate_after_validation": False}
By default, pydantic's ValidationError
is wrapped within DRF's ValidationError
.
If you want to raise pydantic's ValidationError
directly,
set "validation_error": "pydantic"
in the drf_config
property of your model:
import pydantic
from drf_pydantic import BaseModel
class MyModel(BaseModel):
name: str
addresses: list[str]
@pydantic.field_validator("name")
@classmethod
def validate_name(cls, v):
assert isinstance(v, str)
if v != "Billy":
raise ValueError("Wrong door")
return v
drf_config = {"validate_pydantic": True, "validation_error": "pydantic"}
my_serializer = MyModel.drf_serializer(data={"name": "Van", "addresses": []})
my_serializer.is_valid() # this will raise pydantic.ValidationError
Note
When a model is invalid from both DRF's and pydantic's perspectives and
exceptions are enabled (.is_valid(raise_exception=True)
),
DRF's ValidationError
will be raised regardless of the validation_error
setting,
because DRF validation always runs first.
Caution
Setting validation_error
to pydantic
has side effects:
- It may break your views because they expect DRF's
ValidationError
. - Calling
.is_valid()
will always raisepydantic.ValidationError
if the data is invalid, even without setting.is_valid(raise_exception=True)
.
If you have an existing code base and want to add the drf_serializer
attribute only to some of your models, you can extend your existing pydantic models
by adding drf_pydantic.BaseModel
as a parent class to the models you want to extend.
Your existing pydantic models:
from pydantic import BaseModel
class Pet(BaseModel):
name: str
class Dog(Pet):
breed: str
Update your Dog
model and get serializer via the drf_serializer
:
from drf_pydantic import BaseModel as DRFBaseModel
from pydantic import BaseModel
class Pet(BaseModel):
name: str
class Dog(DRFBaseModel, Pet):
breed: str
Dog.drf_serializer
Important
Inheritance order is important: drf_pydantic.BaseModel
must always come before
pydantic.BaseModel
.
If you have nested models and want to generate a serializer for only one of them,
you don't need to update all models. Simply update the model you need,
and drf_pydantic
will automatically generate serializers
for all standard nested pydantic models:
from drf_pydantic import BaseModel as DRFBaseModel
from pydantic import BaseModel
class Apartment(BaseModel):
floor: int
tenant: str
class Building(BaseModel):
address: str
apartments: list[Apartment]
class Block(DRFBaseModel):
buildings: list[Building]
Block.drf_serializer
If drf_pydantic
doesn't generate the serializer you need,
you can configure the DRF serializer fields for each pydantic field manually,
or create a custom serializer for the model altogether.
Important
When manually configuring the serializer, you are responsible for setting all
properties of the fields (e.g., allow_null
, required
, default
, etc.).
drf_pydantic
does not perform any introspection for fields that are manually
configured or for any fields if a custom serializer is used.
from typing import Annotated
from drf_pydantic import BaseModel
from rest_framework.serializers import IntegerField
class Person(BaseModel):
name: str
age: Annotated[float, IntegerField(min_value=0, max_value=100)]
In the example below, Person
will use MyCustomSerializer
as its DRF serializer.
Employee
will have its own serializer generated by drf_pydantic
since it doesn't
inherit a user-defined drf_serializer
attribute.
Company
will use Person
's manually defined serializer for its ceo
field.
from drf_pydantic import BaseModel, DrfPydanticSerializer
from rest_framework.serializers import CharField, IntegerField
class MyCustomSerializer(DrfPydanticSerializer):
name = CharField(allow_null=False, required=True)
age = IntegerField(allow_null=False, required=True)
class Person(BaseModel):
name: str
age: float
drf_serializer = MyCustomSerializer
class Employee(Person):
salary: float
class Company(BaseModel):
ceo: Person
Important
Added in version v2.6.0
Manual drf_serializer
must have base class of DrfPydanticSerializer
in order for Pydantic Validation to work properly.
You can still use standard Serializer
from rest_framework
, but automatic
pydantic model validation will not work consistently and you will get a warning.
Additional field properties are mapped as follows (pydantic
-> DRF
):
description
->help_text
title
->label
StringConstraints
->min_length
andmax_length
pattern
-> Uses the specializedRegexField
serializer fieldmax_digits
anddecimal_places
are carried over (used forDecimal
types, with the current decimal context precision)ge
/gt
->min_value
(only for numeric types)le
/lt
->max_value
(only for numeric types)