Day 27 · ~13m

API Response Validation

Validating external API responses with models, handling missing fields and unexpected formats.

🧑‍💻

When I call an external API, the response might not match what the docs promise. How do I validate API responses with Pydantic?

👩‍🏫

Treat every external API response as untrusted input. Define a model for the expected response shape and validate through it:

from pydantic import BaseModel, Field
from typing import Optional

class WeatherResponse(BaseModel):
    city: str
    temperature: float
    humidity: float
    description: str = ""

# Simulate an API response
api_data = {
    "city": "Portland",
    "temperature": 18.5,
    "humidity": 72.0,
    "description": "Partly cloudy"
}

weather = WeatherResponse.model_validate(api_data)
print(weather.temperature)  # 18.5

If the API returns unexpected data — a missing field, a wrong type, an extra field — Pydantic catches it immediately instead of letting it propagate through your code.

🧑‍💻

But APIs are messy. Sometimes fields are present, sometimes they're not. How do I handle optional fields gracefully?

👩‍🏫

Use Optional with defaults for fields that might be missing. Use model_config with extra="ignore" to silently drop unknown fields:

from pydantic import ConfigDict

class ApiUser(BaseModel):
    model_config = ConfigDict(extra="ignore")

    id: int
    name: str
    email: Optional[str] = None
    avatar_url: Optional[str] = None
    bio: str = ""

# API returns extra fields we don't care about
response = {
    "id": 42,
    "name": "Alice",
    "email": "alice@test.com",
    "created_at": "2026-01-01",  # extra — ignored
    "internal_id": "abc123"     # extra — ignored
}

user = ApiUser.model_validate(response)
print(user.name)       # "Alice"
print(user.avatar_url)  # None — not in response, uses default
🧑‍💻

What about nested API responses? Like a response that has a data wrapper and pagination info?

👩‍🏫

Model the full response structure with nested models:

class PaginationInfo(BaseModel):
    page: int
    total_pages: int
    total_items: int

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

class UserListResponse(BaseModel):
    data: list[UserItem]
    pagination: PaginationInfo
    success: bool = True

api_response = {
    "data": [
        {"id": 1, "name": "Alice", "email": "a@test.com"},
        {"id": 2, "name": "Bob", "email": "b@test.com"}
    ],
    "pagination": {"page": 1, "total_pages": 5, "total_items": 50},
    "success": True
}

result = UserListResponse.model_validate(api_response)
print(len(result.data))               # 2
print(result.pagination.total_items)   # 50
🧑‍💻

What if I want to validate but fall back gracefully when the API returns garbage? Like returning a default instead of crashing?

👩‍🏫

Wrap validation in a try/except and return a fallback:

def safe_parse_response(data: dict, model_class, default=None):
    try:
        return model_class.model_validate(data)
    except Exception:
        return default

result = safe_parse_response(bad_data, WeatherResponse, default=None)
if result is None:
    print("API returned invalid data — using cached values")

This is defensive programming. You define what you expect, validate it, and handle failures gracefully. Never trust external data — validate everything at the boundary.

Practice your skills

Sign up to write and run code in this lesson.

Already have an account? Sign in