Cheatsheet for JSON Schema generation. Used by FastAPI for OpenAPI; by LLM SDKs for tool calling.

model_json_schema

class User(BaseModel):
    id: int
    email: str
    name: str | None = None

User.model_json_schema()
{
  "type": "object",
  "properties": {
    "id": {"type": "integer", "title": "Id"},
    "email": {"type": "string", "title": "Email"},
    "name": {"anyOf": [{"type": "string"}, {"type": "null"}], "default": null, "title": "Name"}
  },
  "required": ["id", "email"]
}

Mode

User.model_json_schema(mode="validation")       # default, for input
User.model_json_schema(mode="serialization")    # for output

Differ with computed fields, exclude, aliases.

json_schema_extra

class User(BaseModel):
    email: str
    age: int
    
    model_config = {
        "json_schema_extra": {
            "examples": [
                {"email": "[email protected]", "age": 30},
                {"email": "[email protected]", "age": 25},
            ]
        }
    }

Surfaces in Swagger UI.

Field-level metadata

class User(BaseModel):
    email: str = Field(..., description="User email", examples=["[email protected]"])
    age: int = Field(..., ge=0, description="Age in years", title="Age")

Custom schema for type

class MyType:
    @classmethod
    def __get_pydantic_json_schema__(cls, schema, handler):
        result = handler(schema)
        result["title"] = "MyType"
        result["description"] = "Custom"
        result["examples"] = ["example1"]
        return result

In FastAPI

FastAPI auto-uses model_json_schema() for request bodies and response models. Shows up at /openapi.json and /docs.

@app.post("/users", response_model=UserOut)
async def create_user(data: UserCreate):
    ...

OpenAPI schema includes UserCreate (request body) and UserOut (response).

$defs / $ref

For nested models, Pydantic generates $ref:

{
  "type": "object",
  "properties": {
    "user": {"$ref": "#/$defs/User"}
  },
  "$defs": {
    "User": {...}
  }
}

OpenAPI translates $defs to components/schemas.

Discriminated union in schema

Animal = Annotated[Cat | Dog, Field(discriminator="kind")]

Schema:

{
  "oneOf": [
    {"$ref": "#/$defs/Cat"},
    {"$ref": "#/$defs/Dog"}
  ],
  "discriminator": {"propertyName": "kind", "mapping": {...}}
}

Cleaner SDKs and docs.

LLM tool calling

from anthropic import Anthropic

class WeatherInput(BaseModel):
    city: str = Field(..., description="City name")
    units: Literal["c", "f"] = "c"

tools = [{
    "name": "get_weather",
    "description": "Get current weather",
    "input_schema": WeatherInput.model_json_schema(),
}]

client.messages.create(model="claude-sonnet-4-6", tools=tools, ...)

LLM providers consume the schema directly.

Nullable types

class M(BaseModel):
    x: int | None

Schema in JSON Schema 2020-12 / OpenAPI 3.1:

{"x": {"anyOf": [{"type": "integer"}, {"type": "null"}]}}

FastAPI emits OpenAPI 3.1 by default.

Title / description

class M(BaseModel):
    """User model.
    
    Long description here.
    """
    name: str = Field(..., title="Name", description="User's name")

Class docstring → schema description.

Generic in schema

class Page(BaseModel, Generic[T]):
    items: list[T]

# Page[User].model_json_schema() generates "Page_User_" schema

Customize JSON schema globally

from pydantic.json_schema import GenerateJsonSchema

class MyJsonSchemaGenerator(GenerateJsonSchema):
    def generate(self, schema, mode="validation"):
        result = super().generate(schema, mode=mode)
        # post-process
        return result

User.model_json_schema(schema_generator=MyJsonSchemaGenerator)

Skip from schema

class M(BaseModel):
    public: int
    internal: int = Field(..., exclude=True)

exclude=True excludes from serialization (and schema follows).

Common mistakes

  • Using model_json_schema(mode="validation") for output — different shape.
  • Massive json_schema_extra — bloats OpenAPI.
  • Custom JSON schema that drifts from runtime validation — confusing.
  • Forgetting model_rebuild() for recursive types in schema.

Read this next

If you want my Pydantic + LLM tool-schema patterns, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .