Pydantic Cursor Rules: Python Data Validation

Cursor rules for Pydantic covering BaseModel, field types/validators, nested models, ConfigDict, JSON Schema, BaseSettings, discriminated unions, and ORM integration.

June 11, 2026by PromptGenius Team
pydanticcursor-rulespythonvalidationfastapitypes
Pydantic Cursor Rules: Python Data Validation

Overview

Pydantic is the standard Python library for data validation and settings management, using type hints to define schemas with runtime validation, JSON Schema generation, and serialization. These cursor rules enforce BaseModel conventions, field validators, nested model patterns, ConfigDict settings, BaseSettings for environment variables, and FastAPI integration so AI assistants generate type-safe, validated Python models.

Note:

Enforces BaseModel with explicit type annotations, @field_validator/@model_validator patterns, ConfigDict (frozen, extra, from_attributes), BaseSettings for env vars, discriminated unions, and model_dump/model_validate_json patterns.

Rules Configuration

---
description: Enforces Pydantic best practices including BaseModel conventions, field validators, nested models, ConfigDict, BaseSettings, discriminated unions, and FastAPI integration patterns.
globs: **/*.py,**/models/**/*.py,**/schemas/**/*.py
---
# Pydantic Best Practices

You are an expert in Pydantic, Python type systems, and data validation.
You understand type annotations, validation pipelines, JSON Schema generation, and FastAPI integration.

### BaseModel Conventions
- Inherit from `BaseModel`: `class User(BaseModel)`
- Use type annotations for all fields: `name: str`, `age: int`, `email: EmailStr`
- Use `Field()` for extra metadata: `Field(default=..., description="...", gt=0, lt=150)`
- Required fields: no default value. Optional: `Optional[str] = None` or `str = "default"`
- Use `model_config = ConfigDict(...)` for model-level configuration
- Prefer `model_dump()` over `.dict()` (V1 → V2 migration)
- Prefer `model_validate()` over `.parse_obj()` (V1 → V2 migration)

### Field Types & Customization
- Standard types: `str`, `int`, `float`, `bool`, `bytes`, `datetime`, `date`, `UUID`
- Specialized types: `EmailStr`, `HttpUrl`, `IPvAnyAddress`, `FilePath`, `DirectoryPath`
- Constrained types: `conint(gt=0, lt=100)`, `constr(min_length=1, max_length=255)`
- Use `Annotated` for reusable constraints: `PositiveInt = Annotated[int, Field(gt=0)]`
- Use `Enum` for constrained choices: `class Status(str, Enum): ACTIVE = "active"`
- Use `Literal["option1", "option2"]` for string unions
- Optional with default: `Optional[str] = None` — never `str = None` alone

### Validators
- Use `@field_validator("field_name")` for single-field validation
- Field validators run before model validators
- Use `@model_validator(mode="before")` to pre-process input data
- Use `@model_validator(mode="after")` for cross-field validation
- Validators must return the value (or raise ValueError/AssertionError)
- Use `info: ValidationInfo` for context: `info.data` (other validated fields)
- `@field_validator("field", mode="before")` runs before type coercion
- `@field_validator("field", mode="after")` runs after type coercion (default)

### Nested Models
- Use Pydantic models as field types: `address: Address`
- Lists of models: `items: list[Item]`
- Nested models auto-validate during parent model validation
- JSON Schema generated recursively for nested models
- Self-referencing models: use `from __future__ import annotations` or string forward references
- Call `model_rebuild()` after defining all referenced models

### Configuration (ConfigDict)
- `frozen=True` — immutable model instances (useful for config objects)
- `extra="forbid"` — reject unknown fields (good for API input validation)
- `extra="ignore"` — silently drop unknown fields (default)
- `from_attributes=True` — allow validation from ORM objects (was `orm_mode`)
- `str_strip_whitespace=True` — auto-strip string fields
- `use_enum_values=True` — serialize enums as their values
- `json_schema_extra={...}` — custom JSON Schema additions
- `validate_default=True` — validate default values against field types

### Serialization
- `model_dump()` → Python dict (was `.dict()`)
- `model_dump_json()` → JSON string (was `.json()`)
- `model_dump(exclude={"password"})` — exclude sensitive fields
- `model_dump(include={"id", "name"})` — include only specific fields
- `model_dump(mode="json")` — JSON-safe types (datetime → ISO string)
- `model_dump(exclude_unset=True)` — only fields explicitly set
- Custom serializers: `@field_serializer("field_name")`

### BaseSettings
- Inherit from `BaseSettings` for environment variable config
- Fields read from env vars (case-insensitive) by default
- Use `Field(alias="DB_HOST")` for different env var names
- `model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")`
- Use `env_prefix="APP_"` to namespace: `APP_DATABASE_URL``database_url`
- Secrets: `secret_key: SecretStr` — masks value in repr and log output

### FastAPI Integration
- Use Pydantic models for request body validation: `async def create(user: UserCreate) -> User`
- Use for query parameters: `async def list(limit: int = Query(default=10))`
- Use response_model for output shaping: `@app.post("/users", response_model=UserResponse)`
- Use discriminated unions for polymorphic responses: `response_model=Annotated[Union[A, B], Field(discriminator="type")]`
- Return models from endpoints — FastAPI handles serialization
- Generate OpenAPI schema automatically from Pydantic models

### Discriminated Unions
- Use `Literal` as discriminator: `class Cat(BaseModel): type: Literal["cat"] = "cat"; lives: int`
- Annotate union: `Pet = Annotated[Union[Cat, Dog], Field(discriminator="type")]`
- Each variant must have the discriminator field with a unique literal value
- FastAPI uses discriminated unions for OpenAPI oneOf with discriminator

### Dynamic Models
- Use `create_model("Name", field1=(str, ...), field2=(int, 0))` for runtime model creation
- Provide `__base__=ExistingModel` to extend existing models
- Use for programmatic schema generation from database metadata
- Dynamic models support full validation, serialization, and JSON Schema like static models

Installation

Create pydantic.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.

pip install pydantic

# With email validation
pip install "pydantic[email]"

# With settings management
pip install pydantic-settings

Examples

# models.py — BaseModel with validators and nested models
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator

class Address(BaseModel):
    street: str
    city: str
    country: str = Field(min_length=2, max_length=2)
    zip_code: str

class User(BaseModel):
    model_config = ConfigDict(str_strip_whitespace=True)

    id: int
    name: str = Field(min_length=1, max_length=100)
    email: EmailStr
    age: int = Field(gt=0, lt=150)
    address: Address
    tags: list[str] = []
    created_at: datetime = Field(default_factory=datetime.now)
    password: Optional[str] = Field(default=None, exclude=True)

    @field_validator("name")
    @classmethod
    def name_must_be_title_case(cls, v: str) -> str:
        if not v.istitle():
            raise ValueError("Name must be in title case")
        return v

    @field_validator("tags")
    @classmethod
    def tags_must_be_lowercase(cls, v: list[str]) -> list[str]:
        return [tag.lower().strip() for tag in v]

# Usage
user = User(
    id=1,
    name="Alice Smith",
    email="[email protected]",
    age=30,
    address={"street": "123 Main", "city": "NYC", "country": "US", "zip_code": "10001"},
    tags=[" Python ", "Data"],
)
print(user.model_dump(exclude={"password"}))
# settings.py — BaseSettings for environment configuration
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        env_prefix="APP_",
    )

    database_url: str
    redis_url: str = "redis://localhost:6379"
    debug: bool = False
    max_connections: int = Field(default=10, gt=0)

settings = Settings()
# api.py — FastAPI with discriminated unions
from typing import Annotated, Literal, Union
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI()

class TextContent(BaseModel):
    type: Literal["text"] = "text"
    body: str

class ImageContent(BaseModel):
    type: Literal["image"] = "image"
    url: str
    alt: str

Message = Annotated[Union[TextContent, ImageContent], Field(discriminator="type")]

class CreateMessage(BaseModel):
    content: Message

@app.post("/messages", response_model=CreateMessage)
async def create_message(msg: CreateMessage):
    return msg