Declaring a Zod or Joi schema and never calling .parse() on it is the canonical AI security theater move — the schema exists only as a type alias, and your API accepts arbitrary JSON at runtime. OWASP A03 (Injection) and A04 (Insecure Design) are the direct mappings: without a parse call, mass-assignment attacks, prototype pollution via crafted payloads, and type coercion exploits all reach your database unchecked. If a breach occurs and an auditor finds schemas that were never enforced, the liability argument becomes 'you knew the controls were needed but shipped them as decoration.'
Critical because a dangling schema provides zero runtime protection — every route that trusts the schema is unvalidated in production, exposing the full attack surface of OWASP A03 Injection.
Wire each exported schema into a .parse() or .safeParse() call at the route entry point. The schema and the parse call must be in the same request path — a type declaration elsewhere doesn't count.
// src/app/api/users/route.ts
import { CreateUserSchema } from '@/lib/schemas'
export async function POST(req: Request) {
const body = await req.json()
const data = CreateUserSchema.parse(body) // throws ZodError on bad input
const user = await prisma.user.create({ data })
return Response.json(user, { status: 201 })
}
For tRPC routes, use .input(Schema) on the procedure. For Server Actions, use parseWithZod or safeAction. Search for every exported schema name in your source tree and confirm at least one call site exists.
ID: ai-slop-security-theater.unenforced-validation.validation-schemas-have-runtime-use
Severity: critical
What to look for: When any validation library is in package.json dependencies (one of: zod, yup, joi, valibot, superstruct, class-validator, io-ts, runtypes, arktype, @sinclair/typebox), walk all source files and count all exported schema declarations: export const X = z.object(...), export const X = z.array(...), Joi.object(...), yup.object(...), valibot.object(...). For each exported schema name X, before evaluating, extract and quote the schema declaration line. Then search the entire src/, app/, lib/, server/, pages/, api/ tree (excluding test directories) for at least 1 runtime call site referencing the schema by name: 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) }), useForm({ resolver: valibotResolver(X) }), tRPCRouter.input(X), safeAction(X), useFormState(action, X), parseWithZod({ schema: X }). Count: total exported schemas, total with at least 1 runtime use, total dangling. EXCLUDE schemas whose only use is type inference (type T = z.infer<typeof X> and similar).
Pass criteria: 100% of exported validation schemas have at least 1 runtime call site. Report: "Validation library: [name]. X exported schemas inspected, Y with runtime use, 0 dangling."
Fail criteria: At least 1 exported schema is declared but never invoked at runtime — defined for type inference only.
Do NOT pass when: A schema is only used inside z.infer<typeof Y> and has zero .parse()/.safeParse()/resolver calls anywhere in the codebase. The canonical AI security theater pattern.
Skip (N/A) when: No validation library is in dependencies.
Report even on pass: Always report the validation library name and the count of schemas inspected. Example: "Validation library: zod. 14 exported schemas inspected, 14 with runtime use (100%)."
Cross-reference: For deeper input validation analysis, the Security Hardening audit (security-hardening) covers validation patterns and OWASP-aligned input handling.
Detail on fail: "3 dangling schemas: 'CreateUserSchema' in src/lib/schemas.ts (used only in z.infer), 'UpdateProfileSchema' in src/lib/schemas.ts (zero call sites). The schemas exist but no input is actually validated."
Remediation: A schema defined but never invoked is not validation — it's just types. The runtime accepts any input. Wire each schema into an actual parsing call at every entry point:
// Bad: schema exists but is never parsed
// src/lib/schemas.ts
export const CreateUserSchema = z.object({ email: z.string().email() })
// src/app/api/users/route.ts
export async function POST(req: Request) {
const body = await req.json()
const user = await prisma.user.create({ data: body }) // unvalidated!
}
// Good: parse at the entry point
import { CreateUserSchema } from '@/lib/schemas'
export async function POST(req: Request) {
const body = await req.json()
const data = CreateUserSchema.parse(body) // throws on invalid input
const user = await prisma.user.create({ data })
}