Zod v4 vs ArkType vs TypeBox vs Valibot: Schema Validation Benchmark 2026
·PkgPulse Team
TL;DR
Zod v4 remains the default for TypeScript validation — but Valibot (8KB vs Zod's 60KB) and ArkType (fastest runtime parsing) are compelling for performance-critical use cases. TypeBox generates JSON Schema natively, making it the best choice for OpenAPI/Swagger integration. For new projects: Zod v4. For edge/bundle-size-critical: Valibot. For OpenAPI: TypeBox. For maximum runtime speed: ArkType.
Key Takeaways
- Zod v4: 60KB, 10M+ downloads/week, best ecosystem (react-hook-form, trpc, drizzle)
- Valibot: 8KB, tree-shakable, modular API, ~10x smaller than Zod
- ArkType: Fastest parser (3-10x faster than Zod), TypeScript syntax strings
- TypeBox: JSON Schema native,
Static<typeof Schema>TypeScript types - Performance: ArkType > Valibot > TypeBox > Zod (but all are "fast enough" for most apps)
- Ecosystem: Zod integrates with everything; others are catching up
Downloads
| Package | Weekly Downloads | Trend |
|---|---|---|
zod | ~10M | ↑ Growing |
@sinclair/typebox | ~6M | ↑ Growing |
valibot | ~1M | ↑ Fast growing |
arktype | ~200K | ↑ Growing |
Performance Benchmarks
Schema: User object with 10 fields, nested address, array of tags
Parsing 100,000 objects:
ArkType: 45ms ← Fastest
Valibot: 120ms
TypeBox: 180ms
Zod v4: 280ms (v4 is 2x faster than v3's ~580ms)
Bundle size (minified + gzipped):
Valibot: 8KB ← Smallest
ArkType: 12KB
TypeBox: 60KB (includes JSON Schema types)
Zod v4: 60KB
Type inference speed (tsc, 50-field schema):
ArkType: ~200ms
Zod v4: ~450ms
Valibot: ~600ms
TypeBox: ~300ms
Zod v4: The Default
// Zod v4 — new features and performance improvements:
import { z } from 'zod';
// Basic schema (same as v3):
const UserSchema = z.object({
id: z.string().cuid2(),
email: z.string().email(),
name: z.string().min(2).max(100),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(['user', 'admin', 'moderator']),
tags: z.array(z.string()).max(10),
address: z.object({
street: z.string(),
city: z.string(),
country: z.string().length(2), // ISO 2-letter
}).optional(),
metadata: z.record(z.string(), z.unknown()),
createdAt: z.coerce.date(), // Auto-coerce string → Date
});
type User = z.infer<typeof UserSchema>;
// Zod v4 new: z.file() for Blob/File
const UploadSchema = z.object({
file: z.instanceof(File)
.refine(f => f.size < 5_000_000, 'Max 5MB')
.refine(f => ['image/jpeg', 'image/png', 'image/webp'].includes(f.type), 'Must be JPEG/PNG/WebP'),
caption: z.string().max(500).optional(),
});
// Zod v4 new: z.pipe() for chained transforms
const ParsedDateSchema = z
.string()
.pipe(z.coerce.date()); // string → validated Date
// Zod v4 new: z.toJSONSchema()
const jsonSchema = z.toJSONSchema(UserSchema);
// Generates standard JSON Schema — useful for OpenAPI docs
// Error formatting (v4 — cleaner):
const result = UserSchema.safeParse({ email: 'bad' });
if (!result.success) {
const errors = result.error.flatten();
// { fieldErrors: { email: ['Invalid email'] }, formErrors: [] }
}
Valibot: Bundle-Size Champion
// Valibot — modular, tree-shakable:
import {
object, string, number, array, optional, enum_,
email, minLength, maxLength, integer, minValue, maxValue,
parse, safeParse, flatten,
type InferInput, type InferOutput,
} from 'valibot';
// Only imports what you use — tree-shaking reduces bundle to ~2-5KB for simple schemas
const UserSchema = object({
id: string([minLength(1)]),
email: string([email()]),
name: string([minLength(2), maxLength(100)]),
age: optional(number([integer(), minValue(0), maxValue(150)])),
role: enum_(['user', 'admin', 'moderator']),
tags: array(string(), [maxLength(10)]),
});
type User = InferInput<typeof UserSchema>;
// Parse (throws on error):
const user = parse(UserSchema, rawData);
// Safe parse (returns result/error):
const result = safeParse(UserSchema, rawData);
if (result.success) {
console.log(result.output);
} else {
const errors = flatten(result.issues);
// { nested: { email: ['Invalid email'] } }
}
// Valibot with React Hook Form:
import { valibotResolver } from '@hookform/resolvers/valibot';
import { useForm } from 'react-hook-form';
function SignupForm() {
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: valibotResolver(UserSchema),
});
return (
<form onSubmit={handleSubmit(console.log)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
</form>
);
}
ArkType: Fastest Runtime + TypeScript Syntax
// ArkType — TypeScript-syntax strings for schemas:
import { type } from 'arktype';
// Syntax feels like writing TypeScript:
const User = type({
id: 'string',
email: 'string.email',
name: '2 <= string <= 100', // min/max length shorthand!
age: 'number.integer | undefined',
role: '"user" | "admin" | "moderator"',
tags: 'string[] <= 10', // array with max length
createdAt: 'Date',
});
type User = typeof User.infer;
// Parse:
const result = User(rawData);
// ArkType returns morph (with parse) or error:
if (result instanceof type.errors) {
console.log(result.summary); // Human-readable error
} else {
// result is User
}
// ArkType advanced: morphs (transform)
const ParsedDate = type('string').pipe(s => new Date(s), 'Date');
// Recursive types (Zod struggles here):
const TreeNode = type({
value: 'number',
children: 'TreeNode[]', // Self-referencing!
}).describe('TreeNode'); // Named for error messages
TypeBox: JSON Schema Native
// TypeBox — generates JSON Schema, used in Fastify/Hono:
import { Type, Static } from '@sinclair/typebox';
import { Value } from '@sinclair/typebox/value';
// TypeBox schema IS JSON Schema:
const UserSchema = Type.Object({
id: Type.String({ format: 'uuid' }),
email: Type.String({ format: 'email' }),
name: Type.String({ minLength: 2, maxLength: 100 }),
age: Type.Optional(Type.Integer({ minimum: 0, maximum: 150 })),
role: Type.Union([
Type.Literal('user'),
Type.Literal('admin'),
Type.Literal('moderator'),
]),
tags: Type.Array(Type.String(), { maxItems: 10 }),
});
// TypeScript type from schema:
type User = Static<typeof UserSchema>;
// Validate:
const result = Value.Check(UserSchema, rawData);
if (!result) {
const errors = [...Value.Errors(UserSchema, rawData)];
// [{ path: '/email', message: 'Expected string' }]
}
// TypeBox + Hono (validated routes with OpenAPI):
import { Hono } from 'hono';
import { describeRoute } from 'hono-openapi';
const app = new Hono();
app.post('/users',
describeRoute({
requestBody: { content: { 'application/json': { schema: UserSchema } } },
responses: { 201: { description: 'Created' } },
}),
async (c) => { /* handler */ }
);
// Export OpenAPI spec:
// app.doc('/openapi.json', { openapi: '3.0.0', info: { title: 'API', version: '1' } })
Decision Guide
Use Zod v4 if:
→ Default choice — best ecosystem (react-hook-form, trpc, drizzle, next-safe-action)
→ Team already knows Zod v3 (v4 is mostly backwards compatible)
→ Need broad library compatibility
→ Bundle size is not a constraint
Use Valibot if:
→ Edge runtime / bundle size critical (<5KB budget)
→ Want tree-shakable, pay-only-for-what-you-use
→ Cloudflare Workers or similar constrained environments
Use ArkType if:
→ Parsing millions of objects (backend hot path)
→ Love TypeScript-native syntax strings
→ Need recursive types easily
→ Fastest possible validation
Use TypeBox if:
→ Building OpenAPI/Swagger documentation
→ Using Fastify (TypeBox is Fastify's native schema)
→ Need JSON Schema output for other tools
→ API validation that also generates docs
Compare Zod, Valibot, ArkType, and TypeBox on PkgPulse.