JSON Schema Validation
Use Fastify's built-in JSON Schema for request validation and compiled response serialization.
Recipe
Quick-reference recipe card - copy-paste ready.
import Fastify from "fastify";
const app = Fastify();
const userSchema = {
type: "object",
required: ["name", "email"],
properties: {
name: { type: "string", minLength: 1, maxLength: 100 },
email: { type: "string", format: "email" },
},
} as const;
app.post("/users", {
schema: {
body: userSchema,
response: {
201: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
name: { type: "string" },
email: { type: "string" },
},
},
},
},
}, async (req, reply) => {
const body = req.body as { name: string; email: string };
return reply.status(201).send({ id: crypto.randomUUID(), ...body });
});When to reach for this: Every public API endpoint. Schema validation is Fastify's primary advantage over Express.
Working Example
import Fastify from "fastify";
import { Type } from "@sinclair/typebox";
import { TypeBoxTypeProvider } from "@fastify/type-provider-typebox";
const app = Fastify().withTypeProvider<TypeBoxTypeProvider>();
const CreateUser = Type.Object({
name: Type.String({ minLength: 1 }),
email: Type.String({ format: "email" }),
});
const UserResponse = Type.Object({
id: Type.String({ format: "uuid" }),
name: Type.String(),
email: Type.String(),
});
app.post("/users", {
schema: {
body: CreateUser,
response: { 201: UserResponse },
},
}, async (req, reply) => {
const { name, email } = req.body;
return reply.status(201).send({ id: crypto.randomUUID(), name, email });
});
app.setErrorHandler((err, _req, reply) => {
if (err.validation) {
return reply.status(400).send({ error: "Validation failed", details: err.validation });
}
reply.status(500).send({ error: "Internal server error" });
});What this demonstrates:
- TypeBox schemas with TypeScript type inference
- Automatic 400 on validation failure
- Custom error handler for validation error shape
- Response schema compiles serializer for speed
Deep Dive
How It Works
- Fastify uses Ajv for request validation at route registration time
- Response schemas compile to fast serialization functions (fast-json-stringify)
- Validation runs in
preValidationhook before the handler - Invalid responses in development log warnings; in production they are stripped to match schema
Schema Coverage
| Part | Validates | Failure status |
|---|---|---|
body | POST/PUT/PATCH body | 400 |
querystring | URL query params | 400 |
params | Route params (:id) | 400 |
headers | Request headers | 400 |
response | Handler return value | 500 (serialization) |
Reusable Schemas
app.addSchema({ $id: "User", type: "object", properties: { id: { type: "string" } } });
app.get("/users/:id", {
schema: {
params: { $ref: "User#" },
response: { 200: { $ref: "User#" } },
},
}, async (req) => ({ id: req.params.id }));Gotchas
- No response schema - lose serialization speed benefit. Fix: define response schemas for all public endpoints.
- Overly permissive
additionalProperties- accepts unexpected fields. Fix: setadditionalProperties: falseon objects. - Schema and TypeScript types diverge - runtime validates one shape, TS assumes another. Fix: use TypeBox or Zod type provider.
- Complex nested schemas slow compilation - startup time grows. Fix: reuse schemas with
$refandaddSchema. - Returning fields not in response schema - stripped silently in production. Fix: match handler output to schema exactly.
- Validating with Zod AND JSON Schema - duplicate validation cost. Fix: pick one; use
fastify-type-provider-zod.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Zod type provider | Team already standardized on Zod | Maximum serialization perf (TypeBox/JSON Schema faster) |
| Ajv standalone in Express | Stuck on Express | Starting new Fastify project |
| OpenAPI code generation | Contract-first with external consumers | Internal-only API |
| Manual validation in handler | Quick prototype only | Any production endpoint |
FAQs
Does validation slow down requests?
Validation adds microseconds. Compiled serializers speed up responses, often netting a performance gain over JSON.stringify.
Can I generate OpenAPI from schemas?
Yes. @fastify/swagger reads route schemas and generates OpenAPI 3 documentation automatically.
How do I validate optional fields?
Omit from required array. Use { type: ["string", "null"] } for nullable fields.
What JSON Schema draft does Fastify use?
Fastify 5 uses Ajv with JSON Schema draft-07 by default. Check @fastify/ajv-compiler for options.
Can I skip validation in development?
Not recommended. Use looser schemas for dev-only routes if needed, but keep validation on public endpoints.
How do file uploads work with schemas?
Multipart uses @fastify/multipart with separate validation. JSON Schema does not cover file fields.
Should I validate response bodies?
Yes for public APIs. It guarantees contract compliance and enables fast serialization.
How does this compare to NestJS ValidationPipe?
NestJS uses class-validator decorators. Fastify uses JSON Schema at the route level. Both validate before handler execution.
Related
- Fastify Basics - getting started
- Fastify Plugins - shared schema registration
- Zod at Boundaries - Zod alternative
- OpenAPI and Swagger - API documentation
- Fastify Best Practices - section checklist
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.