Introduction
Pydantic models validate data on instantiation, and when nested models or custom validators fail, the error messages can be cryptic and hard to trace through deep nesting hierarchies. Common failures include validators that reference undefined fields, nested optional models that should not validate when absent, and validators that modify data in ways that break subsequent validation steps. Pydantic v2 introduced breaking changes with a new validator decorator syntax and a Rust-based core that changes error formats and validation behavior, causing migration issues from v1.
Symptoms
pydantic_core._pydantic_core.ValidationError: 2 validation errors for UserResponse
address.street
Field required [type=missing, input_value={'city': 'NYC'}, input_type=dict]
For further information visit https://errors.pydantic.dev/2.5/v/missingOr validator errors:
ValueError: Validators defined with @validator must also provide their fieldsOr v1 to v2 migration errors:
TypeError: validator() takes 2 positional arguments but 3 were givenCommon Causes
- Required field missing in nested model: Sub-model requires fields not provided
- Validator references wrong field name: Typo in @validator decorator
- Optional nested model not None-checked: Validator runs on None value
- Pydantic v1 vs v2 decorator changes: @validator replaced by @field_validator in v2
- Validator return type mismatch: Validator returns wrong type for the field
- Pre-validator changes data shape: @validator(pre=True) transforms data unexpectedly
Step-by-Step Fix
Step 1: Handle nested optional models
```python from pydantic import BaseModel, Field, field_validator from typing import Optional
class Address(BaseModel): street: str city: str zip_code: str = Field(pattern=r'^\d{5}(-\d{4})?$')
class User(BaseModel): name: str email: str address: Optional[Address] = None # Optional nested model
@field_validator('address', mode='before') @classmethod def validate_address(cls, v): if v is None: return None # Handle dict input if isinstance(v, dict): return Address(**v) return v
# Usage - all work correctly User(name='Alice', email='alice@example.com') # No address User(name='Bob', email='bob@example.com', address={'street': '123 Main', 'city': 'NYC', 'zip_code': '10001'}) ```
Step 2: Pydantic v2 field_validator syntax
```python from pydantic import BaseModel, field_validator, model_validator
class Product(BaseModel): name: str price: float discount: float = 0.0
@field_validator('price') @classmethod def price_must_be_positive(cls, v: float) -> float: if v <= 0: raise ValueError('Price must be positive') return round(v, 2)
@field_validator('discount') @classmethod def discount_must_be_valid(cls, v: float) -> float: if not (0 <= v <= 100): raise ValueError('Discount must be between 0 and 100') return v
@model_validator(mode='after') def validate_price_after_discount(self) -> 'Product': if self.price * (1 - self.discount / 100) < 0.01: raise ValueError('Discounted price must be at least 0.01') return self ```
Step 3: Debug validation errors with structured output
```python from pydantic import ValidationError
def validate_and_report(model_class, data): """Validate data and return structured error report.""" try: return model_class.model_validate(data), None except ValidationError as e: errors = [] for err in e.errors(): errors.append({ 'field': '.'.join(str(loc) for loc in err['loc']), 'message': err['msg'], 'type': err['type'], 'input': err.get('input'), }) return None, errors
# Usage product, errors = validate_and_report(Product, { 'name': 'Widget', 'price': -5.0, # Invalid 'discount': 150, # Also invalid })
if errors: for err in errors: print(f"{err['field']}: {err['message']}") # Output: # price: Value error, Price must be positive # discount: Value error, Discount must be between 0 and 100 ```
Prevention
- Use Pydantic v2 @field_validator with @classmethod decorator consistently
- Handle None values explicitly in validators for optional fields
- Use mode='before' validators for input transformation, mode='after' for validation
- Add model_validator(mode='after') for cross-field validation rules
- Use Field(pattern=...) for simple regex validation instead of custom validators
- Test validation with both valid and invalid inputs in your test suite
- Use validate_and_report helper to get structured validation errors for API responses