Pydantic v2 Validation Bugs AI Assistants Write

AI assistants write Pydantic v2 models that satisfy mypy but allow invalid data through, creating a false sense of API input validation security.

Copper Sun Brass Team · · 5 min read
securityai-code-reviewengineering

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.

Frequently Asked Questions

Does BrassCoders catch Pydantic v2 validation bugs?

BrassCoders catches patterns adjacent to validation bugs — deprecated Pydantic v1 constructs in v2 codebases (via Semgrep rules), raw SQL in route handlers, and missing auth. It does not perform semantic validation of Pydantic model logic to determine whether a validator's behavior matches its annotation.

What's the most common AI mistake in Pydantic v2?

Using @validator (Pydantic v1) instead of @field_validator (Pydantic v2). This silently falls back to v1 compatibility mode in some configurations and fails with a deprecation warning in others. AI assistants trained predominantly on v1 code produce this pattern consistently.

How do I find Pydantic v1 patterns in a v2 codebase?

Run grep -r '@validator|class Config:|orm_mode' src/ to find the most common v1 patterns. Pydantic v2 also provides a migration guide with a full list of changed APIs. BrassCoders's Semgrep scanner will flag these patterns if you're running a recent version.

Is Optional[str] always a bug?

No — Optional[str] is correct when the field genuinely can be None and all downstream code handles None correctly. It becomes a bug when the field is Optional in the schema but the route logic treats it as required, causing a None to propagate into a code path that will raise an AttributeError or produce incorrect behavior.

Does this affect Django REST Framework too?

DRF uses its own serializer validation layer, not Pydantic, so the specific v1→v2 migration bugs don't apply. DRF has its own AI-generated bug pattern: missing required=True on serializer fields, and SerializerMethodField implementations that silently return None.