Skip to content
Web apps

FastAPI guard against Pydantic v1 code

Stop agents writing class Config, orm_mode, @validator, .dict() and pydantic.v1 into FastAPI apps on Pydantic v2, and catch Optional fields that became required.

Verified install Not agent-tested v1.0.0 MIT updated fastapipydanticpythonpytest
Equips
  • 2 2 Rules
  • 1 1 Skills
  • – 0 MCP

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
Level 1 · automated

Verification

Verified install

Checked at commit cc18d7e · 9 passed, 0 failed, 1 not applicable

CheckResultWhat we found
Listinglisting.yaml matches the schema and declares every file in the folder. Pass
Schema valid; 8 files declared, none missing or extra
  • tested.status: untested (agent tests are separate from Level 1)
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
  • CLAUDE.md: matches the generator
  • GEMINI.md: matches the generator
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
  • Claude Code: 6 files, 1 skill in .claude/skills/
  • Codex CLI: 5 files, 1 skill in .agents/skills/
  • Cursor: 6 files, 1 skill in .agents/skills/, 1 rule
  • Gemini CLI: 6 files, 1 skill in .agents/skills/
  • Antigravity CLI: 5 files, 1 skill in .agents/skills/
  • GitHub Copilot: 5 files, 1 skill in .agents/skills/
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
  • Scanner intake-precheck/2: verdict clean; network no (listing says yes)
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
  • Limits: 30 files, 32 KB per file, 128 KB total, 500 lines per skill or rule
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
  • Applied 12 loadout files at every agent's paths
  • ✓ scanner: the v2 app and tests are clean: bash -c python3 .agents/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py app tests; echo "exit=$?"
  • ✓ scanner: the v1 sample is flagged line by line: bash -c mkdir -p /tmp/pydantic-v1-sample && cp .claude/skills/pydantic-v2-guard/references/v1-sample.py.txt /tmp/pydantic-v1-sample/items_v1.py && python3 .claude/skills/pydantic-v2-guard/scripts/pydantic_v2_guard.py /tmp/pydantic-v1-sample; echo "exit=$?"
  • ✓ pytest with PydanticDeprecatedSince20 as an error: uv run --python 3.12 pytest -q -p no:cacheprovider -W error::pydantic.warnings.PydanticDeprecatedSince20
  • ✓ FastAPI 0.162.0 and Pydantic 2.14.1 on the sample's v1 code: uv run --python 3.12 python legacy_probe.py
  • Covers: The scanner's v1 rules (Config, orm_mode, validators, dict/parse_obj/from_orm, Field(regex=), min_items, BaseSettings, on_event) against a real v1 FastAPI module
  • Covers: No false positives on a v2 app with ConfigDict, field and model validators, from_attributes, model_copy, lifespan and pydantic-settings, or on response.json() in tests
  • Covers: The pinned FastAPI and Pydantic as the second check: v1 models, BaseSettings and regex= are refused; deprecated v1 calls warn, and the tests fail on that warning
  • Covers: Optional[X] without a default is required in v2 (ValidationError when left out)
  • Covers: Not covered: real database sessions (the app uses an attribute object in place of an ORM row)

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/.

Download zip
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
FileGoes to
AGENTS.mdAGENTS.md
CLAUDE.mdCLAUDE.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.

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
Download Raw
# 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
Download Raw
# 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
Download Raw
# 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
Download Raw
---
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
Download Raw
---
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
Download Raw
#!/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
Download Raw
# 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
Download Raw
# 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

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.