Introduction
Pydantic ValidationError on nested models is one of the most common errors in FastAPI applications and data processing pipelines. When a nested model field receives data that does not match its schema -- wrong type, missing required field, or failed custom validation -- Pydantic raises a ValidationError with a detailed error tree. However, the error messages can be confusing because they include the full nested path, and debugging requires understanding which level of the nested structure failed and why. This error typically surfaces when parsing API request bodies, reading configuration files, or deserializing data from external sources.
Symptoms
pydantic.v1.error_wrappers.ValidationError: 3 validation errors for UserCreate
name
field required (type=value_error.missing)
address.city
str type expected (type=type_error.str)
address.zip_code
ensure this value is less than or equal to 99999 (type=value_error.number.not_le; limit_value=99999)Or with Pydantic v2:
pydantic_core._pydantic_core.ValidationError: 3 validation errors for UserCreate
name
Field required [type=missing, input_value={'email': 'user@example.com'}, input_type=dict]
address.city
Input should be a valid string [type=string_type, input_value=12345, input_type=int]
address.zip_code
Input should be less than or equal to 99999 [type=less_than_equal, input_value='999999', input_type=str]In FastAPI, this returns as a 422 response:
{
"detail": [
{
"type": "missing",
"loc": ["body", "name"],
"msg": "Field required",
"input": {"email": "user@example.com"}
}
]
}Common Causes
- Missing required field in nested data: API client omits a required field in a nested object
- Type mismatch in nested field: Sending a string where an integer is expected, or vice versa
- Extra fields not allowed: Pydantic v2 default
extra="forbid"rejects unknown fields in nested models - Custom validator failing: A
@field_validatorraises ValueError on invalid data - Union type ambiguity:
Union[A, B]tries A first, fails, then tries B with confusing error messages - Optional nested model not properly typed:
Address | NonevsOptional[Address]with incorrect default
Step-by-Step Fix
Step 1: Define nested models with proper types and defaults
```python from pydantic import BaseModel, Field, field_validator from typing import Optional
class Address(BaseModel): street: str city: str state: str = Field(..., min_length=2, max_length=2) zip_code: str = Field(..., pattern=r"^\d{5}(-\d{4})?$")
class UserCreate(BaseModel): name: str = Field(..., min_length=1, max_length=100) email: str = Field(..., pattern=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$") address: Optional[Address] = None # Address is optional
@field_validator("name") @classmethod def name_not_blank(cls, v: str) -> str: if not v.strip(): raise ValueError("Name cannot be blank or whitespace only") return v.strip() ```
Step 2: Handle validation errors with useful error messages
```python from pydantic import ValidationError
def create_user(data: dict) -> dict: try: user = UserCreate(**data) return {"status": "created", "user": user.model_dump()} except ValidationError as e: # Structured error response for API clients errors = [] for error in e.errors(): errors.append({ "field": ".".join(str(loc) for loc in error["loc"]), "message": error["msg"], "type": error["type"], }) return { "status": "validation_failed", "errors": errors, }
# Example usage result = create_user({ "email": "user@example.com", "address": {"city": 12345, "zip_code": "999999"}, }) # Returns structured errors instead of raising ```
Step 3: Use model_config for flexible parsing
```python from pydantic import ConfigDict
class FlexibleUser(BaseModel): model_config = ConfigDict( str_strip_whitespace=True, # Auto-strip whitespace from strings validate_default=True, # Validate default values use_enum_values=True, # Accept enum values as strings )
name: str role: str = "viewer"
# Accepts whitespace-padded input user = FlexibleUser(name=" John Doe ") assert user.name == "John Doe" # Automatically stripped ```
Step 4: Debug complex validation errors
```python from pydantic import TypeAdapter
# For debugging, use TypeAdapter to get detailed error info adapter = TypeAdapter(UserCreate)
try: adapter.validate_python(invalid_data) except ValidationError as e: print(e.json()) # Machine-readable error JSON print() print(e) # Human-readable error tree ```
Prevention
- Always use
Field()with constraints (min_length,pattern,gt,le) instead of bare type hints - Use
Optional[T] = Nonefor optional nested models, not bareTwith no default - Add
@field_validatormethods for complex validation that cannot be expressed with constraints - Log validation errors with full context:
logger.warning("Validation failed for %s: %s", data, e) - Use
ConfigDict(str_strip_whitespace=True)to handle common whitespace issues automatically - Test validation with both valid and invalid inputs in your test suite to catch schema changes early