# Validation schemas have at least one runtime use

- **Pattern:** `ab-000037` (`ai-slop-hallucinations.data-references.zod-schemas-have-runtime-use`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000037
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000037)

## Why it matters

A Zod schema exported but never called at runtime is not input validation — it is type decoration. The taxons `placeholder-hygiene` and `injection-and-input-trust` both apply: the AI wrote a schema to satisfy the appearance of security (OWASP A03: Injection), but the validation never executes, so arbitrary input reaches database writes, file operations, and downstream systems unchanged. CWE-20 (Improper Input Validation) is the direct mapping. This is the canonical AI security-theater pattern: the codebase looks hardened because schemas exist, but `CreateUserSchema.parse()` is never called on any entry-point body.

## Severity rationale

Critical because a defined-but-never-invoked schema provides zero protection — every API route that relies on it accepts arbitrary input, enabling injection and data corruption.

## Remediation

Call `.parse()` or `.safeParse()` on every schema at every entry point that receives external input. Type inference alone (`z.infer<typeof Schema>`) does nothing at runtime.

```ts
// src/app/api/users/route.ts
import { CreateUserSchema } from '@/lib/schemas'

export async function POST(req: Request) {
  const body = await req.json()

  // Bad: schema exists but is never invoked — arbitrary input passes through
  // const user = await db.user.create({ data: body })

  // Good: parse throws ZodError on invalid input before it reaches the DB
  const data = CreateUserSchema.parse(body)
  const user = await db.user.create({ data })
  return Response.json(user, { status: 201 })
}
```

For React forms: `useForm({ resolver: zodResolver(CreateUserSchema) })`. For tRPC procedures: `.input(CreateUserSchema)`. For server actions: `parseWithZod(formData, { schema: CreateUserSchema })`. Every schema must have at least one call site that runs at runtime.

## Detection

- **ID:** `zod-schemas-have-runtime-use`
- **Severity:** `critical`
- **What to look for:** Walk source files for every exported schema declaration matching: `export const X = z.object(...)`, `export const X = z.array(...)`, `export const X = z.union(...)`, `export const X = z.discriminatedUnion(...)`, `Joi.object(...)`, `yup.object(...)`, `valibot.object(...)`, `Schema(...)` (Mongoose), `pgTable(...)` (Drizzle, but those are tables, exclude them). For each exported schema name `X`, search the entire `src/`, `app/`, `lib/`, `server/`, `pages/`, `api/` tree (excluding test directories) for at least 1 runtime call site referencing the schema: `X.parse(`, `X.safeParse(`, `X.parseAsync(`, `X.safeParseAsync(`, `X.validate(`, `X.validateSync(`, `X.assert(`, `parse(X, ...)`, `validateRequest(X, ...)`, `useForm({ resolver: zodResolver(X) })`, `useForm({ resolver: yupResolver(X) })`, `tRPCRouter.input(X)`, `safeAction(X)`, `useFormState(action, X)`, `parseWithZod({ schema: X })`. EXCLUDE schemas that are only used for type inference (`type T = z.infer<typeof X>`) — search for any use of the schema NAME beyond `z.infer<>`/`Joi.AnyType<>`/`yup.InferType<>`. Count all exported schemas, total with at least one runtime use, total dangling (defined but never invoked at runtime).
- **Pass criteria:** 100% of exported validation schemas have at least 1 runtime call site. Report: "X exported schemas inspected, Y with runtime use, 0 dangling."
- **Fail criteria:** At least 1 exported schema is declared but never invoked at runtime — the schema exists for type inference only, providing no input validation.
- **Do NOT pass when:** A schema is only used inside `type X = z.infer<typeof Y>` declarations and has zero `.parse()`/`.safeParse()`/resolver calls anywhere in the codebase. This is the canonical AI security theater pattern: the model writes "we have validation" via a schema export, but never actually validates anything.
- **Skip (N/A) when:** No validation library is in dependencies (none of `zod`, `yup`, `joi`, `valibot`, `superstruct`, `class-validator`).
- **Report even on pass:** Report the count and library detected. Example: "Validation library: zod. 14 exported schemas inspected, 14 with runtime use (100%)."
- **Cross-reference:** For broader security control validation, the Security Hardening audit (`security-hardening`) covers input validation patterns and security middleware wiring.
- **Detail on fail:** `"3 dangling schemas: 'CreateUserSchema' in src/lib/schemas.ts (used only in type inference), 'UpdateProfileSchema' in src/lib/schemas.ts (zero call sites). The schemas exist but no input is actually validated — this is security theater."`
- **Remediation:** A schema defined but never invoked is not validation — it's just types. The runtime accepts any input. Fix it by actually parsing input through the schema at every entry point:

  ```ts
  // src/app/api/users/route.ts
  import { CreateUserSchema } from '@/lib/schemas'

  export async function POST(req: Request) {
    const body = await req.json()

    // Bad: no validation, body is `any`
    // const user = await db.user.create({ data: body })

    // Good: parse input through the schema
    const data = CreateUserSchema.parse(body)  // throws on invalid input
    const user = await db.user.create({ data })

    return Response.json(user)
  }
  ```

  For form actions, use a resolver: `useForm({ resolver: zodResolver(CreateUserSchema) })`. For tRPC, pass the schema via `.input(CreateUserSchema)`.

## External references

- cwe CWE-20
- owasp A03

Taxons: placeholder-hygiene, injection-and-input-trust

HTML version: https://auditbuffet.com/patterns/ab-000037
