Skip to main content

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

PackageWeekly DownloadsTrend
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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.