AI assistants trained predominantly on pre-2023 Python code have a Pydantic problem. The v2 release shipped in June 2023 with a rewritten validator core — different execution order, different coercion rules, and renamed configuration keys — but public repositories still contain far more v1 code than v2. When Copilot or Claude Code generates a FastAPI route today, the output frequently uses @validator decorators and orm_mode = True, both v1 patterns that behave differently in v2 or fail outright. Those patterns type-check. They run without errors in many configurations. But a subset silently accepts data your API was never designed to receive.
What Changed in Pydantic v2 That AI Assistants Miss
Pydantic v2 changed validation behavior in ways that break AI-generated models silently: the v1 validator decorator was replaced with field_validator with different execution order, orm_mode = True became model_config = ConfigDict(from_attributes=True), and coercion behavior changed for numeric types — all differences an AI trained on v1 patterns will get wrong. BrassCoders's Semgrep scanner flags deprecated v1 patterns in v2 codebases, giving CI a deterministic signal when an AI assistant used outdated syntax.
The most consequential shift is validator execution timing. In v1, a @validator ran early in the field assignment cycle, before Pydantic finalized the value. In v2, @field_validator runs in a defined mode, and that mode determines what value the function receives and what type it must return. An AI-generated validator that returns the wrong type for its mode raises at runtime or silently coerces to a value the annotation doesn't describe.
AI assistants don't consult the Pydantic v2 migration guide at generation time. They pattern-match against training data weighted toward v1. This is structural, not accidental — you'll see the same pattern reproduce across AI assistants on the same prompt.
The Three Validation Anti-Patterns AI Generates
BrassCoders's Semgrep scanner can flag deprecated Pydantic v1 patterns in v2 codebases, but the deeper validation anti-patterns AI produces are: validators that run before field assignment and silently return the wrong type, Optional[str] on a field that the API logic treats as required, and missing strict=True on fields where coercion silently accepts bad input.
The Optional[str] anti-pattern is the most reliably dangerous. An AI assistant generating a user profile model will often declare an email field as Optional[str] — technically correct if the business rule allows null — but then the route handler calls .split('@') on the value without a None check. The model accepts None. The route raises an AttributeError. The annotation promised both were valid; only one actually is in context.
The strict=True gap is subtler. By default, Pydantic v2 coerces "123" to 123 for an int field. If your API should reject a string where an integer was required, you need int = Field(..., strict=True). AI assistants almost never add this. A request that sends "123" instead of 123 passes validation and enters your business logic looking identical to a well-formed request. For ID fields or financial amounts where type precision matters, this coercion gap is a data-integrity risk.
Why mypy Passes and the API Still Accepts Bad Data
mypy validates that the type annotation is structurally sound — it does not validate that the runtime coercion behavior matches the annotation's intent. BrassCoders's Pyre/Pysa scanner goes further: Pysa performs taint analysis across type boundaries and flags data flows where user-controlled input reaches a security-sensitive sink regardless of what the annotation says.
The specific failure mode: mypy evaluates Optional[str] and confirms that str | None is a valid type. It doesn't trace whether the calling code in the route handler guards for None before accessing string methods. The annotation is structurally sound. The runtime behavior is not — and mypy has no objection to either.
Pysa takes a different approach. It tracks the flow of data from the HTTP request body — the taint source — through Pydantic model assignment and into downstream code. If user-controlled input reaches a database query, file path, or subprocess call without passing through a sanitizing function, Pysa flags it. The FastAPI documentation on request validation describes what Pydantic validates on ingress; Pysa describes what happens after validation passes.
The code looks correct statically. At runtime, it isn't — and the difference only surfaces when a malformed request hits a live route.
What BrassCoders Flags in FastAPI and Pydantic Code
BrassCoders's Semgrep and Bandit scanners flag the security-adjacent patterns in AI-generated FastAPI/Pydantic code: raw SQL queries inside route handlers, missing authentication dependencies, and deprecated Pydantic patterns that indicate a v1→v2 migration was handled by an AI that didn't update its behavior.
The Semgrep rules target @validator and orm_mode as v1-pattern signals, both deprecated in v2 and reliably generated by AI assistants working from stale training data. The Bandit scanner catches raw f-string or .format()-style SQL construction in route handlers — the pattern that turns a loose validation assumption into an injection vector.
Install takes one command: pip install brasscoders && brasscoders scan .. The OSS core runs all 12 scanners locally with zero outbound data. BrassCoders's published benchmark found Bandit catching 6 of 12 AI-generated security bugs in isolation; the full 12-scanner pass catches 11 of 12. BrassCoders Paid, the $12/dev/month plan, adds an embedding-based enrichment pass through a hosted gateway that reduces raw findings to the subset most worth triaging first. No trial. Cancel any time via brasscoders portal.
The Pydantic v2 validation gap is a training-data problem. It will reproduce on any project where an AI assistant has write access and the reviewer assumes a clean mypy run means a clean security posture. One @validator in a v2 codebase is a signal worth catching early.
Top comments (0)