Skip to content

Fix: Pydantic ValidationError — Field Required, Value Not a Valid Type, or Extra Fields

FixDevs ·

Quick Answer

How to fix Pydantic v2 validation errors — required fields, type coercion, model_validator, custom validators, extra fields config, and migrating from Pydantic v1.

The Problem

Pydantic raises a ValidationError when creating a model:

from pydantic import BaseModel

class User(BaseModel):
    id: int
    name: str
    email: str

User(id='not-an-int', name='Alice', email='[email protected]')
# ValidationError: 1 validation error for User
# id
#   Input should be a valid integer, unable to parse string as an integer
#   [type=int_parsing, input_value='not-an-int', input_type=str]

Or extra fields cause unexpected errors:

User(id=1, name='Alice', email='[email protected]', role='admin')
# ValidationError: 1 validation error for User
# role
#   Extra inputs are not permitted [type=extra_forbidden, ...]

Or Pydantic v1 validators stop working after upgrading to v2:

# v1 code
from pydantic import validator

class User(BaseModel):
    name: str

    @validator('name')
    def name_must_be_capitalized(cls, v):
        return v.capitalize()

# In v2 — @validator is deprecated, gives warnings or errors

Or a nested model doesn’t validate as expected:

class Address(BaseModel):
    street: str
    city: str

class User(BaseModel):
    address: Address

User(address={'street': '123 Main St'})
# ValidationError: 1 validation error for User
# address.city
#   Field required [type=missing, ...]

Why This Happens

Pydantic validates all input against model field definitions. Errors arise from:

  • Type mismatches — Pydantic v2 is stricter than v1. A str where int is expected raises an error rather than silently coercing.
  • Missing required fields — fields without defaults are required. Omitting them raises Field required.
  • Extra fields forbidden by default — Pydantic v2 forbids extra fields by default in strict mode. In regular mode, extra fields are ignored (not forbidden).
  • Validator API changes in v2@validator (v1) is replaced by @field_validator and @model_validator (v2). The function signatures differ.
  • Nested model validation — nested models are validated recursively. An error in a nested model is reported with the full path (address.city).
  • model_config replacing class Config — Pydantic v2 uses model_config = ConfigDict(...) instead of the inner class Config.

Fix 1: Read Pydantic Error Messages

Pydantic v2 error messages are detailed and actionable:

from pydantic import BaseModel, ValidationError

class User(BaseModel):
    id: int
    name: str
    email: str

try:
    user = User(id='bad', name=None, email='invalid')
except ValidationError as e:
    print(e)
    # 3 validation errors for User
    # id
    #   Input should be a valid integer [type=int_parsing, ...]
    # name
    #   Input should be a valid string [type=string_type, ...]
    # email
    #   Value error, invalid email [type=value_error, ...]

    # Access errors programmatically
    for error in e.errors():
        print(f"Field: {error['loc']}")   # ('id',) or ('address', 'city')
        print(f"Type:  {error['type']}")   # 'int_parsing', 'missing', etc.
        print(f"Msg:   {error['msg']}")    # Human-readable message
        print(f"Input: {error['input']}")  # The value that failed

Error types reference:

Error typeMeaning
missingRequired field not provided
int_parsingCan’t parse value as integer
string_typeExpected string, got something else
extra_forbiddenExtra field not allowed
value_errorCustom validator raised ValueError
assertion_errorCustom validator raised AssertionError
literal_errorValue not in Literal options
enumValue not a valid enum member

Fix 2: Fix Type Validation Errors

Pydantic v2 has two validation modes — strict and lax:

from pydantic import BaseModel, field_validator

class Product(BaseModel):
    id: int
    price: float
    name: str
    in_stock: bool

# Lax mode (default) — type coercion where reasonable
product = Product(
    id='123',        # '123' → 123 (string to int: OK in lax mode)
    price='9.99',    # '9.99' → 9.99 (string to float: OK)
    name=42,         # 42 → '42' (int to str: OK in lax mode)
    in_stock='true', # 'true' → True (string to bool: OK)
)
print(product)  # id=123 price=9.99 name='42' in_stock=True

# Strict mode — no coercion
from pydantic import ConfigDict

class StrictProduct(BaseModel):
    model_config = ConfigDict(strict=True)

    id: int
    price: float

StrictProduct(id='123', price='9.99')
# ValidationError: id must be int, not str

Per-field strict mode:

from pydantic import BaseModel
from pydantic.functional_validators import BeforeValidator
from typing import Annotated

class Order(BaseModel):
    # id must be exactly an int (no string coercion)
    id: Annotated[int, Field(strict=True)]

    # price allows string coercion
    price: float   # '9.99' → 9.99

Fix 3: Handle Optional and Default Fields

from pydantic import BaseModel, Field
from typing import Optional

class User(BaseModel):
    id: int
    name: str

    # Optional with default None
    email: Optional[str] = None        # email is optional, defaults to None
    age: int | None = None             # Same with union syntax (Python 3.10+)

    # Required with no default — MUST be provided
    role: str                          # No default — required

    # Field with validation constraints
    score: float = Field(default=0.0, ge=0, le=100)   # 0 <= score <= 100
    username: str = Field(min_length=3, max_length=50)
    tags: list[str] = Field(default_factory=list)     # Mutable default — use factory

# Required fields:
User(id=1, name='Alice', role='user')   # OK — email/age use defaults
User(id=1, name='Alice')               # Error — role required

Default values and mutability:

# WRONG — mutable default shared across instances (in plain Python too)
class User(BaseModel):
    tags: list[str] = []  # Pydantic handles this correctly (creates new list each time)
    # But for custom mutable types, use default_factory:
    metadata: dict = Field(default_factory=dict)

Fix 4: Write Validators in Pydantic v2

The validator API changed significantly from v1 to v2:

from pydantic import BaseModel, field_validator, model_validator
from typing import Self

class User(BaseModel):
    name: str
    email: str
    age: int
    password: str
    confirm_password: str

    # Field validator — validates a single field
    @field_validator('email')
    @classmethod
    def validate_email(cls, v: str) -> str:
        if '@' not in v:
            raise ValueError('Invalid email format')
        return v.lower()   # Return the transformed value

    # Field validator — runs before type validation
    @field_validator('age', mode='before')
    @classmethod
    def parse_age(cls, v) -> int:
        if isinstance(v, str) and v.isdigit():
            return int(v)
        return v   # Let Pydantic handle type conversion

    # Model validator — validates multiple fields together
    @model_validator(mode='after')
    def passwords_match(self) -> Self:
        if self.password != self.confirm_password:
            raise ValueError('Passwords do not match')
        return self

    # Model validator before field validation
    @model_validator(mode='before')
    @classmethod
    def normalize_input(cls, data: dict) -> dict:
        if isinstance(data, dict) and 'username' in data:
            data['name'] = data.pop('username')   # Rename field
        return data

Pydantic v1 vs v2 validator comparison:

# Pydantic v1
from pydantic import validator

class UserV1(BaseModel):
    name: str

    @validator('name')
    def name_must_not_be_empty(cls, v):
        if not v.strip():
            raise ValueError('Name cannot be empty')
        return v.strip()

# Pydantic v2 — equivalent
from pydantic import field_validator

class UserV2(BaseModel):
    name: str

    @field_validator('name')
    @classmethod
    def name_must_not_be_empty(cls, v: str) -> str:
        if not v.strip():
            raise ValueError('Name cannot be empty')
        return v.strip()

# Key differences:
# 1. @classmethod is now required
# 2. Type annotations on the value parameter (v: str)
# 3. @validator → @field_validator
# 4. @root_validator → @model_validator

Fix 5: Configure Extra Field Handling

Control how Pydantic handles fields not defined in the model:

from pydantic import BaseModel, ConfigDict

# Ignore extra fields (Pydantic v2 default behavior)
class UserIgnore(BaseModel):
    model_config = ConfigDict(extra='ignore')  # Default in v2
    name: str
    email: str

UserIgnore(name='Alice', email='[email protected]', role='admin')
# role is silently ignored — no error

# Forbid extra fields
class UserStrict(BaseModel):
    model_config = ConfigDict(extra='forbid')
    name: str
    email: str

UserStrict(name='Alice', email='[email protected]', role='admin')
# ValidationError: role — Extra inputs are not permitted

# Allow extra fields (store them in model)
class UserFlexible(BaseModel):
    model_config = ConfigDict(extra='allow')
    name: str
    email: str

user = UserFlexible(name='Alice', email='[email protected]', role='admin')
user.role   # 'admin' — accessible as attribute
user.model_extra  # {'role': 'admin'} — all extra fields

Fix 6: Validate Data from APIs and External Sources

Parse and validate incoming data with proper error handling:

from pydantic import BaseModel, ValidationError
from typing import Any
import json

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    created_at: datetime

def parse_user_response(raw_data: Any) -> UserResponse | None:
    try:
        return UserResponse.model_validate(raw_data)
    except ValidationError as e:
        logger.error(f"Failed to parse user response: {e}")
        # Log specific errors
        for error in e.errors():
            logger.error(f"  Field {error['loc']}: {error['msg']}")
        return None

# From JSON string
def parse_user_json(json_str: str) -> UserResponse:
    return UserResponse.model_validate_json(json_str)

# Validate a dict
raw = {'id': 1, 'name': 'Alice', 'email': '[email protected]', 'created_at': '2024-01-15T10:30:00'}
user = UserResponse.model_validate(raw)  # Parses created_at string → datetime

# Update a model with partial data
existing_user = UserResponse(id=1, name='Alice', email='[email protected]', created_at=datetime.now())
updated = existing_user.model_copy(update={'email': '[email protected]'})

FastAPI automatic validation:

from fastapi import FastAPI
from pydantic import BaseModel, field_validator

app = FastAPI()

class CreateUserRequest(BaseModel):
    name: str
    email: str
    age: int

    @field_validator('age')
    @classmethod
    def age_must_be_adult(cls, v: int) -> int:
        if v < 18:
            raise ValueError('Must be at least 18 years old')
        return v

@app.post('/users')
async def create_user(user: CreateUserRequest):
    # FastAPI automatically validates the request body
    # Returns 422 Unprocessable Entity with error details on validation failure
    return {'created': user.model_dump()}

Fix 7: Migrate from Pydantic v1 to v2

Key changes when upgrading:

# v1
from pydantic import BaseModel
from typing import Optional

class User(BaseModel):
    name: str
    email: Optional[str]  # v1: Optional[str] without default = required but nullable

    class Config:
        orm_mode = True        # v1
        use_enum_values = True # v1
        validate_assignment = True  # v1

# v2 equivalent
from pydantic import BaseModel, ConfigDict
from typing import Optional

class User(BaseModel):
    model_config = ConfigDict(
        from_attributes=True,  # v2: was orm_mode
        use_enum_values=True,
        validate_assignment=True,
    )

    name: str
    email: Optional[str] = None  # v2: Optional without default is still required
                                  # Add = None to make it truly optional

# Common v1 → v2 API changes:
# .dict()         → .model_dump()
# .json()         → .model_dump_json()
# .parse_obj()    → .model_validate()
# .parse_raw()    → .model_validate_json()
# .schema()       → .model_json_schema()
# orm_mode        → from_attributes
# @validator      → @field_validator (requires @classmethod)
# @root_validator → @model_validator

Still Not Working?

Circular references — Pydantic v2 handles circular references with model_rebuild(). If a model references itself or another model that references it, call Model.model_rebuild() after all models are defined.

__fields__ vs model_fields — Pydantic v1 used User.__fields__. Pydantic v2 uses User.model_fields. If you’re accessing model metadata, update the attribute name.

json_encoders migration — v1’s json_encoders in Config is replaced by @field_serializer decorators in v2:

from pydantic import field_serializer
from datetime import datetime

class Event(BaseModel):
    created_at: datetime

    @field_serializer('created_at')
    def serialize_dt(self, dt: datetime) -> str:
        return dt.isoformat()

For related Python issues, see Fix: Python Type Hint Error (mypy) and Fix: FastAPI Dependency Injection Error.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles