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

bash
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:

bash
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:

json
{
  "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_validator raises 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 | None vs Optional[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] = None for optional nested models, not bare T with no default
  • Add @field_validator methods 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