Agents still write FastAPI code from the Pydantic v1 years: class Config with orm_mode, @validator, .dict(), parse_obj, Field(regex=). On Pydantic 2.14 most of it still runs with a PydanticDeprecatedSince20 warning, which nobody reads, so it piles up until v3 removes it. Some of it fails outright. Field(regex=) and from pydantic import BaseSettings break at import, and current FastAPI refuses pydantic.v1 models with PydanticV1NotSupportedError. One change fails quietly. Optional[str] with no default is a required field in v2, so clients that leave it out start getting 422s.
This loadout pairs a short Cursor rule and AGENTS.md (a never/always table) with a skill that checks the work. A standard-library Python scanner reports 26 v1 constructs and deprecated FastAPI patterns as errors and Optional-without-default fields as warnings. Then the tests run with PydanticDeprecatedSince20 raised as an error, which catches v1 calls the scanner can't see. The verifier runs both against a FastAPI app with pinned fastapi 0.162.0, pydantic 2.14.1 and pydantic-settings 2.16.2, and against a v1 sample.
What's in it
- Never/always table for Pydantic v2 in a .py-scoped Cursor rule
- Scanner flags 26 Pydantic v1 constructs and on_event, read-only
- Warns when Optional[X] has no default, which v2 makes required
- Tests run with PydanticDeprecatedSince20 as an error
- Migration table checked against pinned FastAPI and Pydantic versions
You'll need
- Tools:
python3,uv
Verification
| Check | Result | What we found |
|---|---|---|
| Listinglisting.yaml matches the schema and declares every file in the folder. | Pass | Schema valid; 8 files declared, none missing or extra
|
| Files parseJSON parses; SKILL.md and .mdc front matter is valid YAML with the fields each agent needs; code fences are closed. | Pass | All 8 files parse; front matter valid |
| AdaptersCLAUDE.md imports @AGENTS.md, GEMINI.md imports @./AGENTS.md, and both match the generator. | Pass | CLAUDE.md and GEMINI.md import AGENTS.md and are up to date
|
| Install pathsEvery file lands where each agent documents it, with no collisions, and the laid-out project is re-discovered correctly. | Pass | Correct paths for all 6 agents; layouts re-discovered
|
| MCP configmcp.json is structurally valid, converts to each agent's format, declares no literal secrets, and referenced packages exist on npm/PyPI. | Not applicable | No MCP servers in this loadout |
| Safety scanThe intake scanner finds no credentials, risky shell, hidden Unicode, instructions hidden in HTML comments or after long blank padding, or prompt-injection and covert wording (such as telling the agent to keep something from you), and the listing's network flag is honest. | Pass | Clean: no credentials, risky shell, hidden text or covert instructions
|
| ReferencesLinks and file paths between the loadout's files resolve, including for each agent's installed layout. | Pass | 3 references between files resolve for every agent |
| Size limitsFile count and sizes are within the submission limits; SKILL.md bodies stay under 500 lines. | Pass | 8 files, 18.7 KB total; largest skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py 7.7 KB
|
| LicenceA recognised SPDX licence, with the licence text available. | Pass | MIT, by SetupBay; text in the repository LICENSE |
| Starter buildA minimal fixture project with the loadout applied installs and builds or type-checks, and the loadout's own code examples compile. | Pass | pydantic_v2_guard.py passes a FastAPI 0.162.0 app on Pydantic 2.14.1 and flags every Pydantic v1 line in the skill's sample: 12 errors and 1 Optional-without-default warning. The app's tests pass with PydanticDeprecatedSince20 as an error, and the pinned FastAPI and Pydantic refuse v1 models in a ro
|
What this means: a script checked the files themselves: they parse, they install where each agent looks, any MCP config is sound, the safety scan is clean, references between files resolve, and, where a free and repeatable fixture exists, a small starter project builds with the loadout applied. No AI agent was run. Per-agent results from agent runs are separate and stay untested until an agent actually runs.
Install
Choose your agent. Run the commands from your project root, or download a zip already laid out in the right folders.
Claude Code reads CLAUDE.md (imports AGENTS.md with @AGENTS.md) and skills from .claude/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/AGENTS.md curl -fsSL --create-dirs -o CLAUDE.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/CLAUDE.md curl -fsSL --create-dirs -o .claude/skills/pydantic-v2-guard/SKILL.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/SKILL.md curl -fsSL --create-dirs -o .claude/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py curl -fsSL --create-dirs -o .claude/skills/pydantic-v2-guard/references/migration-table.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/references/migration-table.md curl -fsSL --create-dirs -o .claude/skills/pydantic-v2-guard/references/v1-sample.py.txt https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/references/v1-sample.py.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
CLAUDE.md | CLAUDE.md |
skills/pydantic-v2-guard/SKILL.md | .claude/skills/pydantic-v2-guard/SKILL.md |
skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py | .claude/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py |
skills/pydantic-v2-guard/references/migration-table.md | .claude/skills/pydantic-v2-guard/references/migration-table.md |
skills/pydantic-v2-guard/references/v1-sample.py.txt | .claude/skills/pydantic-v2-guard/references/v1-sample.py.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Claude Code reads.
Codex CLI reads AGENTS.md and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/SKILL.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/SKILL.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/references/migration-table.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/references/migration-table.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/references/v1-sample.py.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/pydantic-v2-guard/SKILL.md | .agents/skills/pydantic-v2-guard/SKILL.md |
skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py | .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py |
skills/pydantic-v2-guard/references/migration-table.md | .agents/skills/pydantic-v2-guard/references/migration-table.md |
skills/pydantic-v2-guard/references/v1-sample.py.txt | .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Codex CLI reads.
Cursor reads AGENTS.md + .cursor/rules/*.mdc and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .cursor/rules/pydantic-v2-only.mdc https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/.cursor/rules/pydantic-v2-only.mdc curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/SKILL.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/SKILL.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/references/migration-table.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/references/migration-table.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/references/v1-sample.py.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
.cursor/rules/pydantic-v2-only.mdc | .cursor/rules/pydantic-v2-only.mdc |
skills/pydantic-v2-guard/SKILL.md | .agents/skills/pydantic-v2-guard/SKILL.md |
skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py | .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py |
skills/pydantic-v2-guard/references/migration-table.md | .agents/skills/pydantic-v2-guard/references/migration-table.md |
skills/pydantic-v2-guard/references/v1-sample.py.txt | .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Cursor reads.
Gemini CLI reads GEMINI.md (imports AGENTS.md with @./AGENTS.md) and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/AGENTS.md curl -fsSL --create-dirs -o GEMINI.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/GEMINI.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/SKILL.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/SKILL.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/references/migration-table.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/references/migration-table.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/references/v1-sample.py.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
GEMINI.md | GEMINI.md |
skills/pydantic-v2-guard/SKILL.md | .agents/skills/pydantic-v2-guard/SKILL.md |
skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py | .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py |
skills/pydantic-v2-guard/references/migration-table.md | .agents/skills/pydantic-v2-guard/references/migration-table.md |
skills/pydantic-v2-guard/references/v1-sample.py.txt | .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Gemini CLI reads.
Antigravity CLI reads AGENTS.md (read natively; GEMINI.md is also read) and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/SKILL.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/SKILL.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/references/migration-table.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/references/migration-table.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/references/v1-sample.py.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/pydantic-v2-guard/SKILL.md | .agents/skills/pydantic-v2-guard/SKILL.md |
skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py | .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py |
skills/pydantic-v2-guard/references/migration-table.md | .agents/skills/pydantic-v2-guard/references/migration-table.md |
skills/pydantic-v2-guard/references/v1-sample.py.txt | .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Antigravity CLI reads.
GitHub Copilot reads AGENTS.md and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/SKILL.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/SKILL.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/references/migration-table.md https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/references/migration-table.md curl -fsSL --create-dirs -o .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt https://setupbay.com/raw/fastapi-pydantic-v2-guard/1.0.0/skills/pydantic-v2-guard/references/v1-sample.py.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/pydantic-v2-guard/SKILL.md | .agents/skills/pydantic-v2-guard/SKILL.md |
skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py | .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py |
skills/pydantic-v2-guard/references/migration-table.md | .agents/skills/pydantic-v2-guard/references/migration-table.md |
skills/pydantic-v2-guard/references/v1-sample.py.txt | .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what GitHub Copilot reads.
Or as a Claude Code plugin
Adds the skill to Claude Code for all your projects, without copying files. Plugins can't carry project rules, so add AGENTS.md and CLAUDE.md with the commands above. About the plugin marketplace.
/plugin marketplace add https://setupbay.com/marketplace.json /plugin install fastapi-pydantic-v2-guard@setupbay
Files
Rules AGENTS.md Pydantic v2 and FastAPI version facts, rules and the done checklist. 33 lines
# AGENTS.md: FastAPI on Pydantic v2 (Pydantic v1 guard)
This is a **FastAPI** service on **Pydantic v2** (`pydantic>=2.14,<3`, `fastapi>=0.162`). Much of the FastAPI code
you remember was written for Pydantic v1. Some of it fails at import, some only warns, and one pattern changes
whether a field is required.
## Version facts
- Check the pins first: `uv pip show fastapi pydantic pydantic-settings`.
- Current FastAPI refuses `pydantic.v1` models outright (`PydanticV1NotSupportedError`). There is no v1 fallback.
- v1 methods such as `.dict()`, `@validator` and `class Config` still run on v2 but warn
`PydanticDeprecatedSince20`. They are removed in Pydantic v3. Treat them as errors here.
- `Optional[int]` with no default is a **required** field in v2. It allows `None`, it doesn't make the field optional.
- Settings live in `pydantic-settings`: `from pydantic_settings import BaseSettings, SettingsConfigDict`.
- If you're not sure an API exists in v2, check the migration guide (https://docs.pydantic.dev/latest/migration/)
instead of guessing.
## Rules
- Write Pydantic v2 only. The `pydantic-v2-guard` skill has the full table. The common ones are:
`model_config = ConfigDict(from_attributes=True)` (not `class Config: orm_mode = True`),
`@field_validator` / `@model_validator` (not `@validator` / `@root_validator`), `model_dump()` /
`model_dump_json()` / `model_validate()` (not `dict()` / `json()` / `parse_obj()`), `Field(pattern=)` (not `regex=`),
`min_length` (not `min_items`), `Literal[...]` (not `const=`).
- Give every optional field a default: `note: str | None = None`.
- Separate request and response models. Set `response_model` (or a return annotation) so output is filtered.
- Startup and shutdown go in a `lifespan` function, not `@app.on_event`.
- Don't add `pydantic.v1` imports or downgrade Pydantic to make old code pass.
## Before you say "done"
1. Run the `pydantic-v2-guard` scanner on the files you touched. It must report `0 error(s)`. Read every `warn`.
2. Run the tests with v1 deprecations as errors:
`uv run pytest -q -W error::pydantic.warnings.PydanticDeprecatedSince20`.
3. Say which models changed shape (required/optional fields, aliases) so API clients can be checked. Adapter CLAUDE.md Imports AGENTS.md for Claude Code. 11 lines
# CLAUDE.md
@AGENTS.md
## Claude Code notes
- Skills from this loadout live in `.claude/skills/`: `pydantic-v2-guard`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.
<!-- From SetupBay "FastAPI guard against Pydantic v1 code" v1.0.0 — https://setupbay.com/loadouts/fastapi-pydantic-v2-guard/ --> Adapter GEMINI.md Imports AGENTS.md for Gemini CLI. 11 lines
# GEMINI.md
@./AGENTS.md
## Gemini CLI notes
- Skills from this loadout live in `.agents/skills/`: `pydantic-v2-guard`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.
<!-- From SetupBay "FastAPI guard against Pydantic v1 code" v1.0.0 — https://setupbay.com/loadouts/fastapi-pydantic-v2-guard/ --> Cursor rule .cursor/rules/pydantic-v2-only.mdc Never/always table for Python files. 24 lines
---
description: FastAPI with Pydantic v2 only; no Pydantic v1 models, config or methods
globs: "**/*.py"
alwaysApply: false
---
This FastAPI service uses Pydantic v2. FastAPI refuses `pydantic.v1` models.
| Never (Pydantic v1) | Always (Pydantic v2) |
|---|---|
| `class Config: orm_mode = True` | `model_config = ConfigDict(from_attributes=True)` |
| `@validator("x")` | `@field_validator("x")` + `@classmethod` |
| `@root_validator` | `@model_validator(mode="after")` |
| `m.dict()`, `m.json()` | `m.model_dump()`, `m.model_dump_json()` |
| `M.parse_obj(d)`, `M.from_orm(row)` | `M.model_validate(d)`, `M.model_validate(row)` |
| `m.copy(update=...)` | `m.model_copy(update=...)` |
| `Field(regex=...)`, `Field(min_items=1)` | `Field(pattern=...)`, `Field(min_length=1)` |
| `Field(const=True)` | `Literal["value"]` |
| `from pydantic import BaseSettings` | `from pydantic_settings import BaseSettings` |
| `note: Optional[str]` meaning "may be left out" | `note: str \| None = None` |
| `@app.on_event("startup")` | `FastAPI(lifespan=lifespan)` |
- After editing, run the `pydantic-v2-guard` skill's scanner, then pytest with `-W error::pydantic.warnings.PydanticDeprecatedSince20`. Skill skills/pydantic-v2-guard/SKILL.md Scan for Pydantic v1 code, then run the tests with v1 deprecations as errors. 48 lines
---
name: pydantic-v2-guard
description: Check a FastAPI project for Pydantic v1 models, config, validators and methods, and for Optional fields that became required, before saying a model or endpoint change is done. Use after writing or editing Pydantic models or FastAPI routes, when FastAPI raises PydanticV1NotSupportedError, Pydantic warns PydanticDeprecatedSince20, or when porting older FastAPI snippets.
---
# Pydantic v2 guard for FastAPI
Agents still write Pydantic v1 into FastAPI services: `class Config`, `orm_mode`, `@validator`, `.dict()`,
`parse_obj`. On Pydantic 2.14 most of that still runs with a `PydanticDeprecatedSince20` warning, so it looks fine
until v3 removes it. Some of it doesn't run at all: `Field(regex=)` and `from pydantic import BaseSettings` fail at
import, and FastAPI 0.162 refuses `pydantic.v1` models. And `Optional[str]` with no default became a required field.
## 1. Confirm the versions
- `uv run python -c "import pydantic, fastapi; print(pydantic.VERSION, fastapi.__version__)"` should print `2.x` and
a current FastAPI. If Pydantic is 1.x, stop and ask before migrating anything.
## 2. Scan
```bash
python3 .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py app tests
```
(Use the path the skill was installed to: `.claude/skills/...` for Claude Code, `.agents/skills/...` for the others.)
- Every `error` line is Pydantic v1 (or a deprecated FastAPI pattern). Fix it with the replacement the line gives.
`references/migration-table.md` has the table with before/after code.
- `optional-no-default` warnings: decide whether the field may be left out. If yes, add `= None`. If clients must
send it (possibly as `null`), keep it required and add `# guard: ok` to the line. That silences warnings, never errors.
- Run it again until it prints `0 error(s)`.
## 3. Let Pydantic check it
```bash
uv run pytest -q -W error::pydantic.warnings.PydanticDeprecatedSince20
```
- That turns every remaining v1 call into a failing test, including ones inside helpers the scanner can't see.
- For each changed model, add or keep one test that posts a payload missing each optional field, and one that checks
the response doesn't include fields the response model leaves out.
## 4. Fixes that need care
- **Validators**: `@field_validator` needs `@classmethod`. `pre=True` becomes `mode="before"`. A `values` argument
becomes `info: ValidationInfo` with `info.data`.
- **`from_orm`**: needs `from_attributes=True` in `model_config`. Then `Model.model_validate(orm_obj)`.
- **Aliases**: `allow_population_by_field_name` becomes `populate_by_name=True`. Check `by_alias=True` where JSON
uses the alias.
- **Settings**: `class Config: env_file = ".env"` becomes `model_config = SettingsConfigDict(env_file=".env")`.
## 5. Report
Say how many v1 constructs you replaced, which fields changed between required and optional, and that the scanner and
the tests (with PydanticDeprecatedSince20 as an error) were clean. Skill file skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py Read-only scanner for Pydantic v1 constructs and Optional fields without defaults. 168 lines
#!/usr/bin/env python3
"""Find Pydantic v1 code (and FastAPI patterns built on it) in a FastAPI project on Pydantic v2.
Usage: python3 pydantic_v2_guard.py [PATH ...] (default: the current folder)
python3 pydantic_v2_guard.py --list (print every rule)
A "guard: ok" comment on a line silences its warnings (never its errors).
Prints one line per finding: path:line: level [rule] what is wrong -> what to use
Exit 0 when nothing at "error" level is found, 1 when something is, 2 on bad usage.
Standard library only. Read-only: it never edits a file and makes no network calls.
"""
import fnmatch
import os
import re
import sys
# (rule id, level, regex, message, file patterns). Patterns run against each line with comments removed.
RULES = [
('pydantic-v1-import', 'error', '\\bfrom\\s+pydantic\\.v1\\b|\\bimport\\s+pydantic\\.v1\\b|pydantic\\.v1\\.',
'pydantic.v1 models are refused by FastAPI (PydanticV1NotSupportedError) -> plain pydantic v2 models', []),
('basesettings-import', 'error', 'from\\s+pydantic\\s+import\\s+[^#]*\\bBaseSettings\\b',
'BaseSettings moved to pydantic-settings -> from pydantic_settings import BaseSettings, SettingsConfigDict', []),
('class-config', 'error', '^\\s+class\\s+Config\\s*(\\(|:)',
'inner class Config -> model_config = ConfigDict(...) (settings: SettingsConfigDict)', []),
('orm-mode', 'error', '\\borm_mode\\b',
'orm_mode -> from_attributes=True in ConfigDict', []),
('allow-population', 'error', '\\ballow_population_by_field_name\\b',
'allow_population_by_field_name -> populate_by_name=True (or validate_by_name)', []),
('schema-extra', 'error', '\\bschema_extra\\b',
'schema_extra -> json_schema_extra', []),
('anystr-config', 'error', '\\b(anystr_strip_whitespace|anystr_lower|max_anystr_length|min_anystr_length)\\b',
'anystr_* config -> str_strip_whitespace, str_to_lower, str_max_length, str_min_length', []),
('validator', 'error', '@validator\\(|\\bimport\\s+[^#]*\\bvalidator\\b',
"@validator -> @field_validator('x') with @classmethod (mode='before' for pre=True)", []),
('root-validator', 'error', '@root_validator\\b|\\broot_validator\\b',
"@root_validator -> @model_validator(mode='before' | 'after')", []),
('each-item', 'error', '\\beach_item\\s*=',
'validator(each_item=True) -> annotate the item type: list[Annotated[int, AfterValidator(f)]]', []),
('dict-method', 'error', '\\.dict\\(\\s*(\\)|[a-z_]+\\s*=)',
'.dict() -> .model_dump() (exclude_unset=, by_alias= still work)', []),
('json-method', 'error', '\\.json\\(\\s*(exclude|include|by_alias|indent|models_as_dict)\\w*\\s*=',
'model.json(...) -> .model_dump_json(...) (a bare .json() is left alone: HTTP responses have one too)', []),
('parse-obj', 'error', '\\.(parse_obj|parse_raw|parse_file)\\(',
'parse_obj/parse_raw -> model_validate() / model_validate_json()', []),
('from-orm', 'error', '\\.from_orm\\(',
'from_orm() -> model_validate(obj) with from_attributes=True', []),
('copy-update', 'error', '\\.copy\\(\\s*update\\s*=',
'.copy(update=...) -> .model_copy(update=...)', []),
('schema-method', 'error', '\\.(schema|schema_json)\\(\\s*\\)',
'.schema() -> .model_json_schema()', []),
('fields-attr', 'error', '\\.__fields__\\b|\\.__fields_set__\\b',
'__fields__ -> model_fields (on the class); __fields_set__ -> model_fields_set', []),
('forward-refs', 'error', '\\.update_forward_refs\\(',
'update_forward_refs() -> model_rebuild()', []),
('construct', 'error', '\\.construct\\(',
'.construct() -> .model_construct()', []),
('field-regex', 'error', '\\bField\\([^)]*\\bregex\\s*=',
'Field(regex=...) is removed -> Field(pattern=...)', []),
('field-const', 'error', '\\bField\\([^)]*\\bconst\\s*=',
'Field(const=...) is removed -> Literal[...]', []),
('field-items', 'error', '\\b(min_items|max_items|unique_items)\\s*=',
'min_items/max_items -> min_length/max_length (unique_items -> a set type)', []),
('allow-mutation', 'error', '\\ballow_mutation\\b',
'allow_mutation -> frozen=True (field or model)', []),
('constr-regex', 'error', '\\bconstr\\([^)]*\\bregex\\s*=',
'constr(regex=...) -> Annotated[str, StringConstraints(pattern=...)]', []),
('parse-obj-as', 'error', '\\bparse_obj_as\\(',
'parse_obj_as(T, data) -> TypeAdapter(T).validate_python(data)', []),
('on-event', 'error', '\\.on_event\\(\\s*[\\"\'](startup|shutdown)[\\"\']',
'@app.on_event is deprecated -> lifespan=asynccontextmanager function', []),
('optional-no-default', 'warn', '^\\s+\\w+\\s*:\\s*(Optional\\[[^\\]]+\\]|[\\w\\[\\], .]+\\|\\s*None)\\s*$',
'Optional[X] with no default is REQUIRED in v2 (it only allows None) -> add = None if it may be left out', []),
('orm-response-dict', 'warn', 'response_model\\s*=\\s*dict\\b',
'response_model=dict skips output filtering -> a response model', []),
]
FILES = ['*.py']
COMMENT = '#'
SKIP_DIRS = {".git", ".venv", "venv", "node_modules", "__pycache__", ".agents", ".claude", ".cursor", "dist", "build", "target", ".godot"}
COMPILED = [(rid, lvl, re.compile(rx), msg, globs) for rid, lvl, rx, msg, globs in RULES]
def strip_comment(line):
"""Drop a trailing comment, leaving string literals alone."""
q = None
i = 0
while i < len(line):
c = line[i]
if q:
if c == "\\":
i += 2
continue
if c == q:
q = None
elif c in "\"'`":
q = c
elif line.startswith(COMMENT, i):
return line[:i]
i += 1
return line
def wanted(name):
return any(fnmatch.fnmatch(name, g) for g in FILES)
def walk(paths):
for p in paths:
if os.path.isfile(p):
yield p
continue
for root, dirs, files in os.walk(p):
dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
for f in sorted(files):
if wanted(f):
yield os.path.join(root, f)
def scan(path):
out = []
name = os.path.basename(path)
try:
with open(path, encoding="utf-8", errors="replace") as fh:
lines = fh.read().splitlines()
except OSError as e:
return [(path, 0, "error", "unreadable", str(e))]
for n, raw in enumerate(lines, 1):
code = strip_comment(raw)
if not code.strip():
continue
accepted = "guard: ok" in raw # a person accepted the warnings on this line
for rid, lvl, rx, msg, globs in COMPILED:
if accepted and lvl == "warn":
continue
if globs and not any(fnmatch.fnmatch(name, g) for g in globs):
continue
if rx.search(code):
out.append((path, n, lvl, rid, msg))
return out
def main(argv):
if "--list" in argv:
for rid, lvl, _, msg, globs in RULES:
print(f"{lvl:5} [{rid}] {msg}" + (f" (only {', '.join(globs)})" if globs else ""))
return 0
if any(a.startswith("-") for a in argv):
print(__doc__)
return 2
paths = argv or ["."]
for p in paths:
if not os.path.exists(p):
print(f"no such path: {p}", file=sys.stderr)
return 2
files = list(walk(paths))
findings = []
for f in files:
findings.extend(scan(f))
for path, n, lvl, rid, msg in findings:
print(f"{os.path.relpath(path)}:{n}: {lvl} [{rid}] {msg}")
errors = sum(1 for x in findings if x[2] == "error")
warns = sum(1 for x in findings if x[2] == "warn")
print(f"pydantic_v2_guard: {len(files)} file(s), {errors} error(s), {warns} warning(s)")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:])) Skill file skills/pydantic-v2-guard/references/migration-table.md Pydantic v1 to v2 table for FastAPI services. 48 lines
# Pydantic v1 → v2 table for FastAPI
From the Pydantic migration guide (https://docs.pydantic.dev/latest/migration/) and FastAPI's docs. Checked against
pydantic 2.14.1, pydantic-settings 2.16.2 and fastapi 0.162.0.
## Fails on v2 or in FastAPI
| v1 | v2 | What happens with v1 code |
|---|---|---|
| `from pydantic.v1 import BaseModel` in a route | `from pydantic import BaseModel` | FastAPI raises `PydanticV1NotSupportedError` |
| `from pydantic import BaseSettings` | `from pydantic_settings import BaseSettings` | `PydanticImportError` |
| `Field(regex="^a")` | `Field(pattern="^a")` | `PydanticUserError: regex is removed` |
| `Field(const=True)` | `Literal["value"]` | error |
| `constr(regex=...)` | `Annotated[str, StringConstraints(pattern=...)]` | error |
| `note: Optional[str]` (no default) | `note: str \| None = None` | field is now required, so 422 for clients that leave it out |
## Deprecated (warns PydanticDeprecatedSince20, removed in v3)
| v1 | v2 |
|---|---|
| `class Config: orm_mode = True` | `model_config = ConfigDict(from_attributes=True)` |
| `allow_population_by_field_name = True` | `populate_by_name=True` |
| `schema_extra = {...}` | `json_schema_extra={...}` |
| `anystr_strip_whitespace = True` | `str_strip_whitespace=True` |
| `allow_mutation = False` | `frozen=True` |
| `@validator("x", pre=True)` | `@field_validator("x", mode="before")` + `@classmethod` |
| `@validator("x", each_item=True)` | `list[Annotated[int, AfterValidator(check)]]` |
| `@root_validator` | `@model_validator(mode="after")` (instance method) |
| `m.dict(exclude_unset=True)` | `m.model_dump(exclude_unset=True)` |
| `m.json()` | `m.model_dump_json()` |
| `M.parse_obj(d)` / `M.parse_raw(s)` | `M.model_validate(d)` / `M.model_validate_json(s)` |
| `M.from_orm(row)` | `M.model_validate(row)` with `from_attributes=True` |
| `m.copy(update={...})` | `m.model_copy(update={...})` |
| `M.schema()` | `M.model_json_schema()` |
| `M.__fields__` | `M.model_fields` |
| `M.update_forward_refs()` | `M.model_rebuild()` |
| `M.construct(...)` | `M.model_construct(...)` |
| `Field(min_items=1, max_items=5)` | `Field(min_length=1, max_length=5)` |
| `parse_obj_as(list[M], data)` | `TypeAdapter(list[M]).validate_python(data)` |
## FastAPI patterns
| Old | Current |
|---|---|
| `@app.on_event("startup")` | `@asynccontextmanager async def lifespan(app): ...; yield` and `FastAPI(lifespan=lifespan)` |
| `response_model=dict` | a response model, so output is filtered and documented |
| `Depends()` default arguments | `Annotated[Session, Depends(get_session)]` | Skill file skills/pydantic-v2-guard/references/v1-sample.py.txt A FastAPI module written for Pydantic v1, for checking the guard. 44 lines
# A FastAPI module written for Pydantic v1, used to check the guard.
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel, BaseSettings, Field, root_validator, validator # basesettings-import, validator, root-validator
app = FastAPI()
class Settings(BaseSettings):
database_url: str
class Config: # class-config
env_file = ".env"
class ItemIn(BaseModel):
name: str = Field(..., regex="^[a-z-]+$") # field-regex
tags: list[str] = Field(default_factory=list, min_items=1) # field-items
note: Optional[str] # optional-no-default
@validator("name") # validator
def lower(cls, v):
return v.lower()
class ItemOut(BaseModel):
id: int
name: str
class Config: # class-config
orm_mode = True # orm-mode
@app.on_event("startup") # on-event
def connect():
pass
@app.post("/items")
def create(item: ItemIn):
row = save(item.dict()) # dict-method
return ItemOut.from_orm(row) # from-orm All files: source zip · Licence MIT · By SetupBay · First listed
Credits
Public work that inspired this loadout. The wording here is our own; follow the links for the originals.
Installing and checks
How do I install FastAPI guard against Pydantic v1 code in Claude Code?
From your project root, put AGENTS.md, CLAUDE.md, skills/pydantic-v2-guard/SKILL.md at .claude/skills/pydantic-v2-guard/SKILL.md, skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py at .claude/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py, skills/pydantic-v2-guard/references/migration-table.md at .claude/skills/pydantic-v2-guard/references/migration-table.md, skills/pydantic-v2-guard/references/v1-sample.py.txt at .claude/skills/pydantic-v2-guard/references/v1-sample.py.txt. The "Claude Code" tab on this page has the curl commands, or download https://setupbay.com/download/fastapi-pydantic-v2-guard/claude-code.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install FastAPI guard against Pydantic v1 code in Codex CLI?
From your project root, put AGENTS.md, skills/pydantic-v2-guard/SKILL.md at .agents/skills/pydantic-v2-guard/SKILL.md, skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py at .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py, skills/pydantic-v2-guard/references/migration-table.md at .agents/skills/pydantic-v2-guard/references/migration-table.md, skills/pydantic-v2-guard/references/v1-sample.py.txt at .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt. The "Codex CLI" tab on this page has the curl commands, or download https://setupbay.com/download/fastapi-pydantic-v2-guard/codex.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install FastAPI guard against Pydantic v1 code in Cursor?
From your project root, put AGENTS.md, .cursor/rules/pydantic-v2-only.mdc, skills/pydantic-v2-guard/SKILL.md at .agents/skills/pydantic-v2-guard/SKILL.md, skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py at .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py, skills/pydantic-v2-guard/references/migration-table.md at .agents/skills/pydantic-v2-guard/references/migration-table.md, skills/pydantic-v2-guard/references/v1-sample.py.txt at .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt. The "Cursor" tab on this page has the curl commands, or download https://setupbay.com/download/fastapi-pydantic-v2-guard/cursor.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install FastAPI guard against Pydantic v1 code in Gemini CLI?
From your project root, put AGENTS.md, GEMINI.md, skills/pydantic-v2-guard/SKILL.md at .agents/skills/pydantic-v2-guard/SKILL.md, skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py at .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py, skills/pydantic-v2-guard/references/migration-table.md at .agents/skills/pydantic-v2-guard/references/migration-table.md, skills/pydantic-v2-guard/references/v1-sample.py.txt at .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt. The "Gemini CLI" tab on this page has the curl commands, or download https://setupbay.com/download/fastapi-pydantic-v2-guard/gemini-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install FastAPI guard against Pydantic v1 code in Antigravity CLI?
From your project root, put AGENTS.md, skills/pydantic-v2-guard/SKILL.md at .agents/skills/pydantic-v2-guard/SKILL.md, skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py at .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py, skills/pydantic-v2-guard/references/migration-table.md at .agents/skills/pydantic-v2-guard/references/migration-table.md, skills/pydantic-v2-guard/references/v1-sample.py.txt at .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt. The "Antigravity CLI" tab on this page has the curl commands, or download https://setupbay.com/download/fastapi-pydantic-v2-guard/antigravity-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install FastAPI guard against Pydantic v1 code in GitHub Copilot?
From your project root, put AGENTS.md, skills/pydantic-v2-guard/SKILL.md at .agents/skills/pydantic-v2-guard/SKILL.md, skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py at .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py, skills/pydantic-v2-guard/references/migration-table.md at .agents/skills/pydantic-v2-guard/references/migration-table.md, skills/pydantic-v2-guard/references/v1-sample.py.txt at .agents/skills/pydantic-v2-guard/references/v1-sample.py.txt. The "GitHub Copilot" tab on this page has the curl commands, or download https://setupbay.com/download/fastapi-pydantic-v2-guard/copilot.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
What has been checked in FastAPI guard against Pydantic v1 code?
It passed the Level 1 automated install checks on 2026-10-29: the files parse, install to each agent's documented paths, and pass the safety scan. No AI agent has run it yet, so no agent pass or fail is claimed.
More for Claude Code, Codex, Cursor, Gemini CLI, Antigravity CLI and GitHub Copilot.