# At most one validation library

- **Pattern:** `ab-000226` (`ai-slop-code-drift.tooling-stack-drift.dual-validation-library`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000226
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000226)

## Why it matters

Two validation libraries produce two schema syntaxes, two inferred-type systems, two error-object shapes, and two ways to compose refinements. A backend route validated with Zod cannot share its schema with a frontend form validated by Yup, so every endpoint has its contract defined twice and they drift the moment someone adds a field to only one side. Error rendering components branch on library-specific shapes, and TypeScript type narrowing works on one side but not the other.

## Severity rationale

High because schema drift between frontend and backend causes silent validation gaps where invalid data passes one check and fails the other.

## Remediation

Standardize on one library — Zod if you want TypeScript-first inference, Yup if you have deep Formik-era code. Colocate every schema in `src/lib/schemas/` and import from both client and server code.

```ts
// src/lib/schemas/login.ts
import { z } from 'zod'
export const LoginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
})
export type LoginInput = z.infer<typeof LoginSchema>
```

Remove the loser with `npm uninstall yup @hookform/resolvers`.

## Detection

- **ID:** `dual-validation-library`
- **Severity:** `high`
- **What to look for:** Apply the three-condition rule against this exact validation library allowlist: `zod`, `yup`, `joi`, `valibot`, `superstruct`, `class-validator`, `io-ts`, `runtypes`, `arktype`, `@sinclair/typebox`. Count all that meet all three conditions (at least 1 non-escape-hatch importing file). EXCEPT: if a validation library is a transitive peer dependency of a form library (e.g., `yup` shipping with `@hookform/resolvers/yup`) AND is imported in ≤2 files, treat it as an escape hatch.
- **Pass criteria:** 0 or 1 validation libraries from the allowlist actively used. Report even on pass: "Canonical validation library: [name] ([N] importing files)."
- **Fail criteria:** 2 or more validation libraries from the allowlist meet all three conditions (at least 1 non-escape-hatch importing file).
- **Skip (N/A) when:** 0 validation libraries from the allowlist appear in `RUNTIME_DEPS`.
- **Detail on fail:** `"2 active validation libraries: 'zod' (18 files) AND 'yup' (7 files). Pick one — schemas should be consistent across the codebase."`
- **Remediation:** Two validation libraries means two schema syntaxes, two sets of types to maintain, two error formats. Pick one and migrate:

  ```ts
  // Bad: src/lib/schemas.ts uses zod, src/forms/login.ts uses yup
  // Good: every schema in zod (or every schema in yup), nothing in between

  // src/lib/schemas.ts
  import { z } from 'zod'
  export const LoginSchema = z.object({ ... })
  ```

Taxons: dependency-coherence

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