Skip to content

Fix: Pydantic ValidationError — Field Required / Value Not Valid

FixDevs ·

Quick Answer

How to fix Pydantic ValidationError in Python — missing required fields, wrong types, custom validators, handling optional fields, v1 vs v2 API differences, and debugging complex nested models.

The Error

A Pydantic model raises a ValidationError when you try to create or parse data:

from pydantic import BaseModel

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

User(id="not-an-int", email="[email protected]")
pydantic_core._pydantic_core.ValidationError: 2 validation errors 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_url=...]
age
  Field required [type=missing, input_value={'id': 'not-an-int', 'email': '[email protected]'}, input_url=...]

Or when parsing JSON in FastAPI:

422 Unprocessable Entity
{
  "detail": [
    {"type": "missing", "loc": ["body", "age"], "msg": "Field required"},
    {"type": "int_parsing", "loc": ["body", "id"], "msg": "Input should be a valid integer"}
  ]
}

Why This Happens

Pydantic validates data types and constraints strictly at model instantiation time:

  • Missing required field — a field with no default value was not provided in the input.
  • Wrong type that can’t be coercedid: int received "not-an-int". Pydantic tries to coerce compatible types ("42"42) but fails for incompatible ones.
  • Failed custom validator — a @field_validator or @validator raised an error.
  • Constraint violationField(gt=0) received a negative number, or max_length=100 was exceeded.
  • Nested model validation failure — a field typed as another Pydantic model received incompatible data.
  • Pydantic v1 vs v2 API mismatch — Pydantic v2 changed many APIs. Code written for v1 fails with v2 import paths or different validator syntax.

Fix 1: Provide All Required Fields

A field without a default value is required. The ValidationError message shows Field required for every missing field:

from pydantic import BaseModel
from typing import Optional

class User(BaseModel):
    id: int           # Required — no default
    email: str        # Required
    age: int          # Required
    bio: Optional[str] = None   # Optional — defaults to None
    role: str = "user"           # Optional — has a default

# Missing age → ValidationError
User(id=1, email="[email protected]")

# Correct
User(id=1, email="[email protected]", age=30)

# bio and role have defaults — can be omitted
User(id=1, email="[email protected]", age=30)  # role="user", bio=None

Check which fields have defaults by inspecting the model:

for name, field in User.model_fields.items():
    required = field.is_required()
    print(f"{name}: required={required}, default={field.default}")

Fix 2: Fix Type Mismatches

Pydantic coerces compatible types but rejects incompatible ones. Understanding what Pydantic will and won’t coerce:

class Item(BaseModel):
    price: float
    count: int
    active: bool

# Coerced successfully
Item(price="9.99", count="5", active=1)
# price=9.99 (str→float), count=5 (str→int), active=True (int→bool)

# Fails — "yes" can't be parsed as bool or int
Item(price=9.99, count=5, active="yes")
# ValidationError: Input should be a valid boolean [type=bool_parsing]

Use model_validate for dict input instead of unpacking:

data = {"price": "9.99", "count": "5", "active": True}

# Correct
item = Item.model_validate(data)

# Also correct but less explicit
item = Item(**data)

Use model_validate_json for JSON strings:

import json

json_str = '{"price": 9.99, "count": 5, "active": true}'
item = Item.model_validate_json(json_str)

Fix 3: Handle Optional and Nullable Fields Correctly

The difference between optional (has a default) and nullable (can be None) matters:

from pydantic import BaseModel
from typing import Optional

class User(BaseModel):
    name: str                    # Required, cannot be None
    bio: Optional[str] = None    # Optional AND nullable — can be omitted or None
    nickname: str | None = None  # Same as above (Python 3.10+ syntax)
    age: Optional[int]           # Nullable but REQUIRED — must be provided, even as None
# Works — age is provided as None
User(name="Alice", age=None)

# Fails — age is required (even though it can be None)
User(name="Alice")
# ValidationError: Field required [type=missing] for age

If you want a field to be truly optional (can be omitted entirely), give it a default:

class User(BaseModel):
    name: str
    age: Optional[int] = None   # Can be omitted, defaults to None

Fix 4: Add Custom Validators

Use @field_validator (Pydantic v2) to add custom validation logic:

from pydantic import BaseModel, field_validator, ValidationInfo

class User(BaseModel):
    email: str
    age: int
    password: str
    password_confirm: str

    @field_validator('email')
    @classmethod
    def email_must_contain_at(cls, v: str) -> str:
        if '@' not in v:
            raise ValueError('must be a valid email address')
        return v.lower()   # Return the transformed value

    @field_validator('age')
    @classmethod
    def age_must_be_adult(cls, v: int) -> int:
        if v < 18:
            raise ValueError('must be 18 or older')
        return v

Cross-field validation with @model_validator:

from pydantic import BaseModel, model_validator

class PasswordChange(BaseModel):
    password: str
    password_confirm: str

    @model_validator(mode='after')
    def passwords_match(self) -> 'PasswordChange':
        if self.password != self.password_confirm:
            raise ValueError('passwords do not match')
        return self

Fix 5: Use Field Constraints Instead of Custom Validators

For common constraints, use Field() instead of writing custom validators:

from pydantic import BaseModel, Field
from typing import Annotated

class Product(BaseModel):
    name: str = Field(min_length=1, max_length=100)
    price: float = Field(gt=0, le=10000)
    quantity: int = Field(ge=0, default=0)
    sku: str = Field(pattern=r'^[A-Z]{3}-\d{4}$')
    tags: list[str] = Field(default_factory=list, max_length=10)

# Or with Annotated (Pydantic v2 preferred style)
PositivePrice = Annotated[float, Field(gt=0)]

class Product(BaseModel):
    price: PositivePrice

Common Field constraints:

ConstraintApplies toMeaning
gtnumbersgreater than
genumbersgreater than or equal
ltnumbersless than
lenumbersless than or equal
min_lengthstr, listminimum length
max_lengthstr, listmaximum length
patternstrmust match regex
default_factoryanycallable for mutable defaults

Fix 6: Fix Pydantic v1 vs v2 API Differences

Pydantic v2 (released 2023) changed many APIs. If you’re migrating or using libraries that bundle Pydantic v1:

FeaturePydantic v1Pydantic v2
Validator decorator@validator('field')@field_validator('field')
Root validator@root_validator@model_validator(mode='before'/'after')
Dict output.dict().model_dump()
JSON output.json().model_dump_json()
Parse dictModel(**data) or .parse_obj(data)Model.model_validate(data)
Parse JSON.parse_raw(json_str)Model.model_validate_json(json_str)
Config classclass Config:model_config = ConfigDict(...)

Check your Pydantic version:

pip show pydantic

Use the v1 compatibility layer in v2 if you have a large migration ahead:

# Force Pydantic v2 to run in v1 compatibility mode for a model
from pydantic.v1 import BaseModel  # Uses v1 API even on v2 install

Or install the old version:

pip install "pydantic<2"

Fix 7: Debug Complex Validation Errors

For nested models or complex validation errors, the error output gives the full location path:

from pydantic import BaseModel
from typing import list

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

class Order(BaseModel):
    user_id: int
    items: list[str]
    shipping_address: Address

try:
    Order(
        user_id=1,
        items=["item1"],
        shipping_address={"street": "123 Main St"}  # Missing city and zip_code
    )
except ValidationError as e:
    print(e.error_count())   # 2
    for error in e.errors():
        print(error['loc'])  # ('shipping_address', 'city'), ('shipping_address', 'zip_code')
        print(error['msg'])  # "Field required"
        print(error['type']) # "missing"

Programmatically handle specific errors:

from pydantic import ValidationError

def create_user_safe(data: dict) -> User | None:
    try:
        return User.model_validate(data)
    except ValidationError as e:
        errors = {'.'.join(str(loc) for loc in err['loc']): err['msg']
                  for err in e.errors()}
        # {'email': 'value is not a valid email address', 'age': 'Field required'}
        raise ValueError(f"Invalid user data: {errors}") from e

In FastAPI, customize the 422 error response:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from pydantic import ValidationError

app = FastAPI()

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=422,
        content={
            "error": "Validation failed",
            "details": [
                {
                    "field": ".".join(str(loc) for loc in err["loc"][1:]),
                    "message": err["msg"],
                }
                for err in exc.errors()
            ],
        },
    )

Still Not Working?

Check if you’re using model_config correctly in v2. The old class Config approach still works but is deprecated:

# Pydantic v2 — preferred
from pydantic import BaseModel, ConfigDict

class User(BaseModel):
    model_config = ConfigDict(
        str_strip_whitespace=True,
        validate_default=True,
        arbitrary_types_allowed=True,
    )

# Pydantic v1 style (still works in v2 but triggers deprecation warning)
class User(BaseModel):
    class Config:
        anystr_strip_whitespace = True

Enable strict mode to prevent any type coercion and get explicit errors:

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

    id: int
    name: str

StrictUser(id="42", name="Alice")
# ValidationError: Input should be a valid integer [type=int_type] — no coercion

Use model_construct() to skip validation when you’re certain the data is already valid (e.g., data from your own database):

# Skips validation — use only with trusted data
user = User.model_construct(id=1, email="[email protected]", age=30)

For related Python and FastAPI issues, see Fix: FastAPI 422 Unprocessable Entity and Fix: Python TypeError — Missing Required Argument.

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