Manual Validation vs Schema Based Validation
Hand-rolled if-checks versus a declared schema as the single source of truth. One scales with your patience, the other scales with your data. We pick the one that stops you shipping garbage.
The short answer
Schema Based Validation over Manual Validation for most cases. A schema is a contract you write once and enforce everywhere — type coercion, error messages, and a generated TypeScript type fall out for free.
- Pick Manual Validation if have one weird field with cross-system business logic no schema language expresses cleanly, or a single throwaway script where pulling in a dependency is genuine overkill
- Pick Schema Based Validation if validate request bodies, config, API responses, or env vars anywhere — which is to say, almost always. This is the default
- Also consider: You can do both: schema for shape and types, a manual refinement hook for the one rule about 'discount can't exceed order total.' Most schema libraries bless exactly this. Don't treat it as either/or.
— Nice Pick, opinionated tool recommendations
What they actually are
Manual validation is you, writing if (!email.includes('@')) throw new Error('bad email') by hand, endpoint by endpoint, field by field. Every rule is imperative code you own forever. Schema-based validation flips it: you declare the shape once — z.object({ email: z.string().email(), age: z.number().int().min(0) }) — and a library (Zod, Pydantic, Joi, JSON Schema) does the enforcement, coercion, and error reporting. The difference isn't cosmetic. Manual validation answers 'is this one value okay right here.' A schema answers 'does this entire payload conform to a contract,' and it does it the same way in every caller. One is a pile of guard clauses. The other is a typed, reusable definition of truth that your editor, your tests, and your docs can all read. That asymmetry is the whole fight, and it's not close.
Where manual validation earns its keep
It isn't useless, so spare me the dogma. Manual validation wins when the rule is genuinely bespoke: 'this coupon is valid only if the user's plan started before the promo and they haven't redeemed it in another region.' No schema vocabulary expresses that without an escape hatch, so you'll write code anyway. It also wins in tiny, dependency-averse contexts — a 40-line CLI, a Lambda where cold-start grams matter, a build script nobody else touches. And it wins when you need full control over failure behavior: retry, partial accept, log-and-continue. The honest case for manual is precision and zero dependencies. The dishonest case — 'I don't want to learn a library' — is how you end up with seven slightly different email regexes and a production incident. If your manual validation is more than a handful of one-off business rules, you didn't choose manual. You reinvented a schema, badly, without the tests.
Where schema-based pulls ahead
Everything that scales. Write the schema once and every endpoint, queue consumer, and config loader enforces identical rules — no drift, no 'oh, the mobile API forgot to check that.' You get automatic type coercion ('42' to 42), structured error objects you can return straight to a client, and, with Zod or Pydantic, a generated static type so your compiler and your runtime finally agree on what a User is. Validate at the boundary, and the inside of your app stops being defensive. The cost is real: a dependency, a learning curve, and occasional gymnastics for rules the schema language wasn't built for. But those are one-time taxes. Manual validation charges you on every new field, forever, in the currency of vigilance — and vigilance is the first thing to go on a Friday deploy. Schemas make the safe path the lazy path. That's the whole game.
The verdict, no hedging
Schema-based validation, and I'm not entertaining the rebuttal. The instant you have more than one entry point — and you do — manual validation becomes a consistency problem you solve with discipline, which means you don't solve it. A schema is executable documentation, a runtime guard, and a type definition in one declaration; it deletes an entire category of 'we forgot to check' bugs. The mature move isn't picking a side, it's putting a schema at every boundary and reaching for a manual refinement only when a rule is too weird to declare. That's roughly 95% schema, 5% hand-written, and the 5% lives inside the schema's escape hatch where it belongs. If you're still writing bare if-checks on request bodies in 2026 because a library felt like overkill, the overkill was the production bug you're about to ship.
Quick Comparison
| Factor | Manual Validation | Schema Based Validation |
|---|---|---|
| Consistency across endpoints | Each call site re-implements rules; drift is inevitable | One schema enforced identically everywhere |
| Type safety / inference | None — runtime checks don't inform the compiler | Generated static types (Zod/Pydantic) keep runtime and compiler in sync |
| Bespoke cross-field business rules | Trivial — it's just code you write directly | Needs a refinement/escape hatch; can get awkward |
| Dependencies / footprint | Zero — no library required | Adds a dependency and a small learning curve |
| Maintenance cost over time | Charged on every new field, forever, via vigilance | One-time setup tax, then near-free to extend |
The Verdict
Use Manual Validation if: You have one weird field with cross-system business logic no schema language expresses cleanly, or a single throwaway script where pulling in a dependency is genuine overkill.
Use Schema Based Validation if: You validate request bodies, config, API responses, or env vars anywhere — which is to say, almost always. This is the default.
Consider: You can do both: schema for shape and types, a manual refinement hook for the one rule about 'discount can't exceed order total.' Most schema libraries bless exactly this. Don't treat it as either/or.
A schema is a contract you write once and enforce everywhere — type coercion, error messages, and a generated TypeScript type fall out for free. Manual validation is the same logic copy-pasted across every endpoint until one of them drifts and lets a null through. Declarative beats imperative when the rules outlive the developer who wrote them.
Related Comparisons
Disagree? nice@nicepick.dev