Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 17

Pydantic vs. DRF Validation: A Deep Dive

Recommended resources

Hello! Welcome back to our series on mastering FastAPI.

In our previous lessons, we explored how Pydantic models define the "shape" of our API data, from simple fields with constraints to complex, nested JSON objects. This declarative approach, much like using DRF Serializers, handles a significant amount of validation automatically. However, real-world applications often require validation logic that's more complex than simple constraints like max_length or type checks.

This is where custom validators come in. Today, you'll learn how to implement custom validation logic with @field_validator and @model_validator, comparing them to DRF's validate_<field> and validate methods. This will give you full control over your data validation, allowing you to enforce any business rule you can express in Python.

A Quick Refresher: Validation in Django REST Framework

Given your background in DRF, you're already familiar with its layered validation system. When data comes into a DRF serializer, it's typically validated in this order:

  1. Field-level validation: Type checking and running built-in field validators (max_length, unique, etc.).
  2. Custom field-level validation: Calling the .validate_<field_name>() method for a specific field.
  3. Object-level validation: Calling the .validate() method, which has access to all the field data.
Data Validation in Django REST Framework
This flowchart shows the validation methods available in DRF. We'll be focusing on the Pydantic equivalents of the custom methods: `validate_<field>` and `validate`.

Pydantic offers a very similar, and equally powerful, system using decorators.


1. Field-Level Validation with @field_validator

When you need to apply custom logic to a single field, you use the @field_validator decorator. This is the direct equivalent of DRF's validate_<field_name> method.

The Analogy to DRF:

Let's say you want to ensure a username doesn't contain spaces.

In DRF, you would write:




# Django REST Framework
from rest_framework import serializers

class UserSerializer(serializers.Serializer):
    username = serializers.CharField()
    email = serializers.EmailField()

    def validate_username(self, value):
        if ' ' in value:
            raise serializers.ValidationError("Username cannot contain spaces.")
        return value

In Pydantic, the equivalent is:




# Pydantic
from pydantic import BaseModel, field_validator
import re

class User(BaseModel):
    username: str
    email: str

    @field_validator('username')
    @classmethod
    def validate_username(cls, v: str) -> str:
        if ' ' in v:
            raise ValueError("Username cannot contain spaces.")



        # You can also perform normalization
        return v.lower()

Notice the key similarities and differences:

  • Both methods target a specific field.
  • Both raise an exception on failure (ValidationError in DRF, ValueError in Pydantic).
  • Both must return the (potentially modified) value on success.
  • The Pydantic validator is a classmethod.

The following video provides a clear, practical demonstration of building a custom field validator.

Python Pydantic Tutorial: Complete Data Validation Course (Used by FastAPI)

This segment from Corey Schafer's Pydantic course walks through creating a custom validator for a username field. It also introduces the crucial mode parameter.

Watch from 00:49:04 to 00:56:50. Pay close attention to: The syntax of the @field_validator decorator. How a ValueError is raised to signal a validation failure. The concept of using the validator for normalization (e.g., converting to lowercase). The difference between mode='after' (default) and mode='before' for pre-processing data.

Understanding the mode Parameter

As you saw in the video, the mode parameter is very important:

  • mode='after' (Default): The validator runs after Pydantic performs its own basic validation (e.g., checking that an int is actually an integer). The input to your validator function (v) is already type-coerced. This is the safer and more common mode.
  • mode='before': The validator runs before Pydantic's validation. Your function receives the raw input value. This is useful for cleaning up or normalizing data before it's validated. For example, you could take an input like " 123 " and strip() the whitespace before Pydantic tries to parse it as an integer.

2. Model-Level Validation with @model_validator

Sometimes you need to validate fields in relation to each other. The classic example is ensuring a password and password_confirm field match. This requires access to the entire data model, not just a single field. For this, you use @model_validator.

The Analogy to DRF:

This is the direct equivalent of DRF's validate method.

In DRF, you would write:




# Django REST Framework
from rest_framework import serializers

class RegistrationSerializer(serializers.Serializer):
    password = serializers.CharField()
    password_confirm = serializers.CharField()

    def validate(self, attrs):
        if attrs['password'] != attrs['password_confirm']:
            raise serializers.ValidationError("Passwords do not match.")
        return attrs

In Pydantic, the equivalent is:




# Pydantic
from pydantic import BaseModel, model_validator
from typing_extensions import Self

class Registration(BaseModel):
    password: str
    password_confirm: str

    @model_validator(mode='after')
    def check_passwords_match(self) -> Self:
        if self.password != self.password_confirm:
            raise ValueError('Passwords do not match')
        return self

Key points:

  • The DRF validate method receives a dictionary of attributes (attrs).
  • The Pydantic @model_validator (in after mode) is an instance method and receives self, giving you access to all fields via self.fieldname.
  • Both must return the validated data (attrs dictionary or self instance).

The next section of the video demonstrates this pattern.

Python Pydantic Tutorial: Complete Data Validation Course (Used by FastAPI)

Watch how to implement a model validator to check that two password fields are identical.

Continue watching from 00:56:50 to 01:00:02. Focus on how the validator method accesses self to compare self.password and self.confirm_password.


3. Exploring Advanced Validation with the Docs

The video gave you a solid practical foundation. For a more systematic overview and to discover more advanced features, the official Pydantic documentation is the best resource.

Validators - Pydantic Validation

Let's review the official documentation to solidify these concepts and see a few more patterns. Your experience with DRF will make these patterns feel very intuitive.

Please read the following sections from the documentation: 'After validators': Skim this section under 'Field validators'. It reinforces the @field_validator decorator pattern we just discussed. 'Before validators': Review this section to see another example of using mode='before' for data normalization. 'Model validators': Read the introduction to this section. 'After validators' (under 'Model validators'): This shows the exact password_repeat example, confirming the pattern for cross-field validation. 'Validation data' (under 'Validation info'): This section introduces a powerful alternative. You can add an info: ValidationInfo argument to a field_validator to access other fields that have already been validated. This can sometimes be used instead of a full model_validator.

The ValidationInfo object is a particularly useful feature. It allows a field validator to "peek" at other fields in the model without needing a full model validator, which can be cleaner for certain use cases. Be mindful of field definition order, as you can only access data from fields defined before the one you are validating.


Conclusion

You now have a complete toolkit for data validation in Pydantic, from simple constraints to complex, custom business logic.

Key Takeaways:

  • @field_validator is for custom logic on a single field, analogous to DRF's validate_<field_name>.
  • @model_validator is for cross-field validation, analogous to DRF's validate.
  • Validators run in different modes, with 'after' (post-Pydantic validation) and 'before' (pre-Pydantic validation) being the most common.
  • Custom validators must either raise a ValueError/AssertionError on failure or return the validated value on success.

In our lessons so far, we've used a single Pydantic model to handle an incoming request. However, it's a common and powerful pattern to use different schemas for different operations. For example, a UserCreate schema might require a password, but the UserRead schema that you return from the API should definitely exclude it.

In our next lesson, we will explore this professional practice. You will learn how to use separate Pydantic schemas for create, update, and response representations of a single resource.