404 Not Found

404 Not Found


nginx

Request Body and Data Validation — In-Depth Practical Guide to Pydantic V2

Data validation is like airport security—every passenger (request) must go through identity verification (type checking), baggage screening (constraint validation), and a review of their declaration (custom validators) before boarding the plane (entering the business logic).

1. What You'll Learn


2. Alice's True Story

(1) Pain Point: Price Data Submission and Validation Are Riddled with Errors

Alice's PriceTracker receives price data submitted by suppliers, requiring that prices be greater than 0, currencies be specified using ISO 4217 three-letter codes, and product names cannot be empty. However, Bob's frontend occasionally sends negative prices or invalid currencies. The custom validation code written in Flask is scattered across 10 different locations, and it overlooks the edge case where "the price is 0," resulting in dirty data with a price of 0 in the database.

(2) A Solution for Declarative Validation in Pydantic V2

Pydantic V2 replaces imperative validation with a declarative model; all constraints are defined within the model, and once defined, they apply globally.

PYTHON
from pydantic import BaseModel, Field, field_validator

class PriceCreate(BaseModel):
    product_id: int = Field(gt=0)
    price: float = Field(gt=0, description="Price in USD")
    currency: str = Field(pattern=r"^[A-Z]{3}$", examples=["USD", "EUR"])

    @field_validator("currency")
    @classmethod
    def validate_currency(cls, v: str) -> str:
        if v not in {"USD", "EUR", "GBP", "JPY", "CNY"}:
            raise ValueError(f"Unsupported currency: {v}")
        return v

(3) Revenue

The price validation code has been consolidated from 10 separate logic blocks into a single model definition; values of 0 and negative prices are automatically filtered out, and currency validation is ensured by a dual-layer approach combining regular expressions and custom validators, eliminating dirty data from the database.


3. Pydantic V2 Data Flow

(1) Full Life Cycle

100%
flowchart TD
    A[JSON Request Body] --> B[model_validate]
    B --> C[Type Coercion]
    C --> D[field_validator]
    D --> E[model_validator]
    E --> F[Valid Model Instance]
    F --> G[model_dump]
    G --> H[JSON Response]
    F --> I[model_dump_json]
    I --> J[JSON String]
Phase Method Description
Input Parsing model_validate(data) Parse and validate from dict/JSON
Type Conversion Automatic "42"42, "9.99"9.99
Field Validation @field_validator Custom Validation for a Single Field
Model Validation @model_validator Cross-Field Joint Validation
Output Serialization model_dump() / model_dump_json() Convert Model to dict/JSON

(1) ▶ Example: Pydantic V2 Base Model

PYTHON
from pydantic import BaseModel, Field

class ProductCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    category: str = Field(max_length=100)
    base_price: float = Field(gt=0, description="Base price in USD")

# Parse and validate
data = {"name": "Widget", "category": "electronics", "base_price": 29.99}
product = ProductCreate.model_validate(data)
print(product.model_dump())

Output:

TEXT
{'name': 'Widget', 'category': 'electronics', 'base_price': 29.99}

4. field_validator and model_validator

(1) V1 → V2 Migration Comparison

100%
flowchart LR
    V1[Pydantic V1] --> Migrate[V1 → V2 Migration]
    Migrate --> V2[Pydantic V2]
    V1 --- A["@validator('field')"]
    V2 --- B["@field_validator('field')"]
    V1 --- C["@root_validator"]
    V2 --- D["@model_validator(mode='after')"]
    V1 --- E["class Config:"]
    V2 --- F["model_config = ConfigDict(...)"]
    V1 --- G[".dict()"]
    V2 --- H[".model_dump()"]
V1 Syntax V2 Syntax Description
@validator("field") @field_validator("field") Field Validator
@root_validator @model_validator(mode="after") Model-Level Validation
class Config: orm_mode = True model_config = ConfigDict(from_attributes=True) ORM Model
.dict() .model_dump() Serialize to dict
.json() .model_dump_json() Serialize to JSON

(1) ▶ Example: field_validator—Single-Field Validation

PYTHON
from pydantic import BaseModel, Field, field_validator

class PriceCreate(BaseModel):
    product_id: int = Field(gt=0)
    price: float = Field(gt=0)
    currency: str = Field(default="USD", max_length=3)

    @field_validator("price")
    @classmethod
    def price_precision(cls, v: float) -> float:
        # Round to 2 decimal places
        return round(v, 2)

    @field_validator("currency")
    @classmethod
    def valid_currency(cls, v: str) -> str:
        allowed = {"USD", "EUR", "GBP", "JPY", "CNY"}
        if v not in allowed:
            raise ValueError(f"Currency must be one of {allowed}")
        return v.upper()

# Test validation
p = PriceCreate(product_id=1, price=9.999, currency="usd")
print(p.model_dump())

Output:

TEXT
{'product_id': 1, 'price': 10.0, 'currency': 'USD'}

(2) ▶ Example: model_validator cross-field validation

PYTHON
from pydantic import BaseModel, Field, model_validator

class PriceRangeQuery(BaseModel):
    min_price: float = Field(ge=0, description="Min price in USD")
    max_price: float = Field(ge=0, description="Max price in USD")

    @model_validator(mode="after")
    def check_range(self):
        if self.min_price > self.max_price:
            raise ValueError("min_price must be <= max_price")
        return self

# Valid
valid = PriceRangeQuery(min_price=10, max_price=100)
print(valid.model_dump())

# Invalid - raises validation error
# PriceRangeQuery(min_price=100, max_price=10)

Output:

TEXT
{'min_price': 10.0, 'max_price': 100.0}

5. Field() Advanced Constraints and JSON Schema

(1) Field() Parameter Quick Reference

Parameter Type Description JSON Schema Mapping
gt Value Greater than exclusiveMinimum
ge Value Greater than or equal to minimum
lt Value Less than exclusiveMaximum
le Value Less than or equal to maximum
min_length String Minimum Length minLength
max_length String Maximum Length maxLength
pattern String Regular Expression pattern
default Any Default default
examples List Example Values examples
description String Description description
alias String Field Alias Alias Mapping

(1) ▶ Example: Field Constraints and JSON Schema

PYTHON
from pydantic import BaseModel, Field

class ProductCreate(BaseModel):
    name: str = Field(
        min_length=1,
        max_length=200,
        description="Product display name",
        examples=["Wireless Mouse", "USB Cable"],
    )
    sku: str = Field(
        pattern=r"^[A-Z]{2}-\d{4,6}$",
        description="SKU code: 2 letters + 4-6 digits",
        examples=["EL-1234", "CB-567890"],
    )
    base_price: float = Field(
        gt=0,
        le=999999.99,
        description="Base price in USD",
        examples=[9.99, 49.99, 199.99],
    )

# View generated JSON Schema
print(ProductCreate.model_json_schema())

Output:

TEXT
# Execution Successful

6. Nested Models and Model Combinations

(1) Nested Model Structure

(1) ▶ Example: PriceTracker's nested model

PYTHON
from pydantic import BaseModel, Field
from typing import Optional

class PriceInfo(BaseModel):
    amount: float = Field(gt=0, description="Price amount in USD")
    currency: str = Field(default="USD", pattern=r"^[A-Z]{3}$")
    source: str = Field(max_length=100, description="Price source")

class ProductCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    category: str = Field(max_length=100)
    current_price: PriceInfo  # Nested model
    original_price: Optional[PriceInfo] = None  # Optional nested

# Nested validation
data = {
    "name": "Wireless Mouse",
    "category": "electronics",
    "current_price": {"amount": 29.99, "currency": "USD", "source": "Amazon"},
    "original_price": {"amount": 49.99, "currency": "USD", "source": "Amazon"},
}
product = ProductCreate.model_validate(data)
print(product.model_dump())

Output:

TEXT
# Execution Successful

(2) ▶ Example: Union and Literal

PYTHON
from pydantic import BaseModel, Field
from typing import Union, Literal

class SinglePrice(BaseModel):
    type: Literal["single"] = "single"
    amount: float = Field(gt=0)

class RangePrice(BaseModel):
    type: Literal["range"] = "range"
    min_amount: float = Field(gt=0)
    max_amount: float = Field(gt=0)

class ProductPrice(BaseModel):
    product_id: int = Field(gt=0)
    pricing: Union[SinglePrice, RangePrice]  # Discriminated union

# FastAPI uses "type" field to determine which model to validate
data = {"product_id": 1, "pricing": {"type": "range", "min_amount": 10, "max_amount": 50}}
pp = ProductPrice.model_validate(data)
print(pp.model_dump())

Output:

TEXT
# Execution Successful

(2) ConfigDict Configuration

(3) ▶ Example: model_config and the ORM pattern

PYTHON
from pydantic import BaseModel, ConfigDict

class ProductResponse(BaseModel):
    model_config = ConfigDict(
        from_attributes=True,  # Enable ORM mode (read from SQLAlchemy objects)
        populate_by_name=True,  # Allow both field name and alias
        json_schema_extra={
            "examples": [{"id": 1, "name": "Widget", "price": 9.99}]
        },
    )

    id: int
    name: str
    price: float

# With from_attributes=True, can create from object attributes
class FakeORMObject:
    def __init__(self):
        self.id = 1
        self.name = "Widget"
        self.price = 9.99

orm_obj = FakeORMObject()
response = ProductResponse.model_validate(orm_obj)
print(response.model_dump())

Output:

TEXT
{'id': 1, 'name': 'Widget', 'price': 9.99}

7. Comprehensive Example

Pydantic V2's field validation and cross-field validation, combined with the ORM pattern and FastAPI request bodies, enable a complete data input validation workflow.

PYTHON
from fastapi import FastAPI
from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict

class PriceCreate(BaseModel):
    product_name: str = Field(min_length=1, max_length=100)
    price: float = Field(gt=0, description="The price must be greater than 0")
    currency: str = "USD"

    @field_validator("currency")
    @classmethod
    def validate_currency(cls, v: str) -> str:
        if v not in ("USD", "EUR", "GBP"):
            raise ValueError("currency must be USD/EUR/GBP")
        return v

class PriceRangeQuery(BaseModel):
    min_price: float = Field(ge=0)
    max_price: float = Field(ge=0)

    @model_validator(mode="after")
    def validate_range(self):
        if self.min_price > self.max_price:
            raise ValueError("min_price must <= max_price")
        return self

app = FastAPI()

@app.post("/prices")
async def create_price(data: PriceCreate):
    return data.model_dump()

Output:

TEXT
POST /prices {"product_name":"Widget","price":9.99} → {"product_name":"Widget","price":9.99,"currency":"USD"}
POST /prices {"product_name":"","price":-1} → 422 Validation Error

❓ FAQ

Q How do I choose between field_validator and model_validator?
A Use field_validator for single-field validation (such as format checks), and use model_validator for cross-field validation (such as min_price <= max_price).
Q Can the @validator annotation from V1 still be used?
A V2 retains a compatibility layer but issues a deprecation warning. New projects must use @field_validator or @model_validator, and existing projects should migrate as soon as possible.
Q What does from_attributes=True do?
A It allows you to create Pydantic models directly from ORM objects (such as SQLAlchemy model instances), reading the object's attributes rather than a dictionary. This is a key configuration for FastAPI + SQLAlchemy.
Q What is the difference between Field()'s examples and json_schema_extra?
A examples is a list of example values for a field, mapped to OpenAPI examples; json_schema_extra is an additional schema property at the model level.
Q What are the issues with overly deep nested models?
A Nested models with more than 3 levels increase validation latency and make debugging more difficult. We recommend flattening the structure or breaking it down using a composite pattern.
Q How much better is Pydantic V2's performance compared to V1?
A Core validation is 5-50 times faster (Rust implementation), and serialization is 2-10 times faster. There is a noticeable improvement in scenarios involving millions of data points.

📖 Summary


📝 Exercises

  1. Basic Problem (Difficulty ⭐): Create the PriceCreate model, which includes product_id: int (> 0) and price: float (> 0), and verify that invalid input triggers a validation error. Hint: BaseModel + Field(gt=0)
  2. Advanced Exercise (Difficulty ⭐⭐): Add PriceCreate to @field_validator to validate that the currency field can only contain USD/EUR/GBP, and add @model_validator to ensure min_price <= max_price. Hint: field_validator + model_validator(mode="after")
  3. Challenge Problem (Difficulty ⭐⭐⭐): Design a complete nested model system for PriceTracker: ProductCreate contains PriceInfo (amount + currency); the currency in PriceInfo is constrained by a regular expression; and the SKU in ProductCreate is constrained by the format in pattern. Hint: Field(pattern=...) + nested BaseModel

---|

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏