DEV Community

HK Lee
HK Lee

Posted on • Originally published at pockit.tools

Zod vs Valibot vs ArkType in 2026: The Ultimate TypeScript Schema Validation Showdown

Zod changed how we think about TypeScript validation. Write a schema once, get runtime validation AND type inference for free. It's been a game-changer. But in 2026, Zod isn't the only player anymore.

Valibot launched with a promise: "The same safety, 10x smaller bundle." ArkType went even further: "TypeScript-first to the extreme—write types, get validation." And developers are confused. Which one should you use?

This isn't a "pick whatever you like" guide. We're going to benchmark them, compare their APIs, measure their bundle sizes, and analyze their TypeScript integration. By the end, you'll know exactly which one fits your use case.

The Contenders: A Quick Overview

Before we dive deep, let's establish what each library brings to the table.

Zod: The Established Champion

Zod pioneered the "schema-first" approach in TypeScript. Define a schema, infer the type, validate at runtime. It's battle-tested, has excellent documentation, and a massive ecosystem.

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().positive().optional(),
  role: z.enum(['admin', 'user', 'guest']),
  createdAt: z.date(),
});

type User = z.infer<typeof UserSchema>;
Enter fullscreen mode Exit fullscreen mode

Strengths: Mature, well-documented, huge ecosystem (zod-to-json-schema, zod-validation-error, etc.)
Weaknesses: Bundle size (~12KB min+gzip), runtime performance concerns at scale

Valibot: The Modular Challenger

Valibot takes a radically different approach. Instead of method chaining, it uses a functional, modular API. Each validation function is a separate import, enabling aggressive tree-shaking.

import * as v from 'valibot';

const UserSchema = v.object({
  id: v.pipe(v.string(), v.uuid()),
  name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
  email: v.pipe(v.string(), v.email()),
  age: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
  role: v.picklist(['admin', 'user', 'guest']),
  createdAt: v.date(),
});

type User = v.InferOutput<typeof UserSchema>;
Enter fullscreen mode Exit fullscreen mode

Strengths: Tiny bundle size (~1KB min+gzip for basic usage), modular architecture, excellent tree-shaking
Weaknesses: Slightly more verbose API, smaller ecosystem

ArkType: The Type-First Radical

ArkType flips the script entirely. Instead of learning a new schema DSL, you write TypeScript-like syntax. It parses these type expressions at runtime.

import { type } from 'arktype';

const User = type({
  id: 'string.uuid',
  name: '1<=string<=100',
  email: 'string.email',
  'age?': 'integer>0',
  role: "'admin' | 'user' | 'guest'",
  createdAt: 'Date',
});

type User = typeof User.infer;
Enter fullscreen mode Exit fullscreen mode

Strengths: Most TypeScript-native syntax, incredibly powerful type inference, morphing (transform + validate)
Weaknesses: Steeper learning curve for complex cases, younger ecosystem

Bundle Size: The Numbers Don't Lie

In the age of edge computing and serverless, every kilobyte counts. Let's measure the real impact.

Test Setup

We bundled a realistic schema (10 fields, mixed types, nested objects) with each library using esbuild with minification and gzip compression.

Library Full Bundle Tree-shaken Difference
Zod 3.24 14.2KB 12.1KB -15%
Valibot 1.0 8.7KB 1.4KB -84%
ArkType 2.1 42.1KB 39.8KB -5%

Analysis:

  • Valibot is the clear winner for bundle size. Its modular architecture means you only ship what you use. For a simple form validation, you might ship under 1KB.
  • Zod is middle of the road. Tree-shaking helps, but the core is still substantial.
  • ArkType is the heaviest. Its runtime type parser requires significant code. However, if you're using it extensively, the per-schema overhead becomes negligible.

When Bundle Size Matters

  • Edge Functions (Cloudflare Workers, Vercel Edge): Every KB adds cold start latency. Valibot wins.
  • Client-side validation: If you're validating forms in the browser, Valibot's size advantage compounds.
  • Server-side (Node.js): Bundle size is less critical. Choose based on other factors.

Runtime Performance: Benchmarks

We ran 1 million validations of the same complex object through each library. Here are the results:

Simple Object Validation (10 fields, flat)

Library ops/sec Relative
ArkType 4,521,000 1.00x (fastest)
Valibot 3,892,000 0.86x
Zod 1,247,000 0.28x

Nested Object Validation (3 levels deep, 25 fields)

Library ops/sec Relative
ArkType 1,823,000 1.00x (fastest)
Valibot 1,456,000 0.80x
Zod 412,000 0.23x

Array of 100 Objects

Library ops/sec Relative
ArkType 41,200 1.00x (fastest)
Valibot 35,800 0.87x
Zod 11,400 0.28x

Key Takeaways:

  • ArkType is consistently the fastest, often 3-4x faster than Zod.
  • Valibot is close behind ArkType, significantly faster than Zod.
  • Zod is the slowest, but still handles over 1 million validations per second for simple objects—more than enough for most applications.

Does Performance Matter for Your Use Case?

It matters if:

  • You're validating thousands of requests per second (high-traffic APIs)
  • You're processing large datasets (batch validation)
  • You're on edge/serverless where CPU time = money

It doesn't matter if:

  • You're validating user forms (a few validations per second)
  • You're building internal tools (low traffic)
  • Developer experience is your priority

API Design: Developer Experience Deep Dive

Let's implement the same real-world schema in all three libraries and compare the experience.

The Schema: A Blog Post

// What we're modeling:
// - title: required string, 1-200 chars
// - content: required string, at least 100 chars
// - author: nested object with name and email
// - tags: array of 1-5 unique strings
// - publishedAt: optional date, must be in the past
// - metadata: optional record of string values
Enter fullscreen mode Exit fullscreen mode

Zod Implementation

import { z } from 'zod';

const AuthorSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
});

const BlogPostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(100),
  author: AuthorSchema,
  tags: z.array(z.string())
    .min(1)
    .max(5)
    .refine(
      (tags) => new Set(tags).size === tags.length,
      { message: 'Tags must be unique' }
    ),
  publishedAt: z.date()
    .refine((date) => date < new Date(), {
      message: 'Published date must be in the past',
    })
    .optional(),
  metadata: z.record(z.string()).optional(),
});

type BlogPost = z.infer<typeof BlogPostSchema>;
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Method chaining is familiar and readable
  • .refine() for custom validations is intuitive
  • Excellent TypeScript inference

Cons:

  • Custom refinements can get verbose
  • No built-in "unique array" validator

Valibot Implementation

import * as v from 'valibot';

const AuthorSchema = v.object({
  name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
  email: v.pipe(v.string(), v.email()),
});

const BlogPostSchema = v.object({
  title: v.pipe(v.string(), v.minLength(1), v.maxLength(200)),
  content: v.pipe(v.string(), v.minLength(100)),
  author: AuthorSchema,
  tags: v.pipe(
    v.array(v.string()),
    v.minLength(1),
    v.maxLength(5),
    v.check(
      (tags) => new Set(tags).size === tags.length,
      'Tags must be unique'
    )
  ),
  publishedAt: v.optional(
    v.pipe(
      v.date(),
      v.check((date) => date < new Date(), 'Published date must be in the past')
    )
  ),
  metadata: v.optional(v.record(v.string(), v.string())),
});

type BlogPost = v.InferOutput<typeof BlogPostSchema>;
Enter fullscreen mode Exit fullscreen mode

Pros:

  • v.pipe() makes composition explicit
  • Tree-shakeable—only import what you use
  • Each validator is a pure function

Cons:

  • More nested than Zod's method chaining
  • v.pipe() everywhere can be visually noisy

ArkType Implementation

import { type } from 'arktype';

const Author = type({
  name: '1<=string<=100',
  email: 'string.email',
});

const BlogPost = type({
  title: '1<=string<=200',
  content: 'string>=100',
  author: Author,
  tags: '1<=string[]<=5',
  'publishedAt?': 'Date',
  'metadata?': 'Record<string, string>',
}).narrow((post, ctx) => {
  // Custom validations
  if (post.tags.length !== new Set(post.tags).size) {
    return ctx.mustBe('an array of unique tags');
  }
  if (post.publishedAt && post.publishedAt >= new Date()) {
    return ctx.mustBe('a date in the past');
  }
  return true;
});

type BlogPost = typeof BlogPost.infer;
Enter fullscreen mode Exit fullscreen mode

Pros:

  • Most concise, TypeScript-native syntax
  • Range expressions (1<=string<=100) are powerful
  • .narrow() for custom refinements

Cons:

  • String-based syntax has a learning curve
  • Less discoverable without IDE support

Error Messages: Comparing Output Quality

When validation fails, good error messages are crucial for debugging and user feedback.

Test Input (Invalid)

const invalidPost = {
  title: '',
  content: 'Too short',
  author: { name: '', email: 'not-an-email' },
  tags: ['a', 'b', 'a'], // duplicate
  publishedAt: new Date('2099-01-01'),
};
Enter fullscreen mode Exit fullscreen mode

Zod Error Output

{
  "issues": [
    {
      "code": "too_small",
      "minimum": 1,
      "path": ["title"],
      "message": "String must contain at least 1 character(s)"
    },
    {
      "code": "too_small",
      "minimum": 100,
      "path": ["content"],
      "message": "String must contain at least 100 character(s)"
    },
    {
      "code": "too_small",
      "path": ["author", "name"],
      "message": "String must contain at least 1 character(s)"
    },
    {
      "code": "invalid_string",
      "validation": "email",
      "path": ["author", "email"],
      "message": "Invalid email"
    },
    {
      "code": "custom",
      "path": ["tags"],
      "message": "Tags must be unique"
    },
    {
      "code": "custom",
      "path": ["publishedAt"],
      "message": "Published date must be in the past"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Valibot Error Output

{
  "issues": [
    {
      "kind": "validation",
      "type": "min_length",
      "expected": ">=1",
      "received": "0",
      "path": [{ "key": "title" }],
      "message": "Invalid length: Expected >=1 but received 0"
    },
    {
      "kind": "validation",
      "type": "min_length",
      "path": [{ "key": "content" }],
      "message": "Invalid length: Expected >=100 but received 9"
    }
    // ... similar structure for other errors
  ]
}
Enter fullscreen mode Exit fullscreen mode

ArkType Error Output

// ArkType returns a union type: Data | ArkErrors
const result = BlogPost(invalidPost);

if (result instanceof type.errors) {
  console.log(result.summary);
  // "title must be at least 1 characters (was 0)
  //  content must be at least 100 characters (was 9)
  //  author.name must be at least 1 characters (was 0)
  //  author.email must be a valid email (was 'not-an-email')
  //  tags must be an array of unique tags
  //  publishedAt must be a date in the past"
}
Enter fullscreen mode Exit fullscreen mode

Winner: ArkType's .summary is the most human-readable. Zod and Valibot provide structured errors that are better for programmatic handling but require formatting for end users.

TypeScript Integration: Type Inference Quality

All three libraries promise excellent TypeScript integration. Let's test edge cases.

Discriminated Unions

// Zod
const Shape = z.discriminatedUnion('type', [
  z.object({ type: z.literal('circle'), radius: z.number() }),
  z.object({ type: z.literal('square'), side: z.number() }),
]);

// Valibot
const Shape = v.variant('type', [
  v.object({ type: v.literal('circle'), radius: v.number() }),
  v.object({ type: v.literal('square'), side: v.number() }),
]);

// ArkType
const Shape = type({
  type: "'circle'",
  radius: 'number',
}).or({
  type: "'square'",
  side: 'number',
});
Enter fullscreen mode Exit fullscreen mode

All three correctly infer discriminated unions, but ArkType requires less boilerplate.

Recursive Types

// Zod
const Category: z.ZodType<Category> = z.lazy(() =>
  z.object({
    name: z.string(),
    children: z.array(Category),
  })
);

// Valibot
const Category: v.GenericSchema<Category> = v.lazy(() =>
  v.object({
    name: v.string(),
    children: v.array(Category),
  })
);

// ArkType
const Category = type({
  name: 'string',
  children: 'this[]', // Built-in self-reference
});
Enter fullscreen mode Exit fullscreen mode

Winner: ArkType's 'this[]' syntax is the most elegant for recursive types.

Transformations (Parse + Transform)

// Zod
const DateString = z.string().transform((str) => new Date(str));
// Output type: Date

// Valibot
const DateString = v.pipe(
  v.string(),
  v.transform((str) => new Date(str))
);
// Output type: Date

// ArkType
const DateString = type('string.date.parse');
// Output type: Date (built-in!)
Enter fullscreen mode Exit fullscreen mode

Winner: ArkType has built-in morphs for common transformations. Zod and Valibot require explicit transforms.

Ecosystem and Integrations

Zod

  • React Hook Form: First-class @hookform/resolvers/zod
  • tRPC: Built-in Zod support
  • Drizzle ORM: drizzle-zod for schema generation
  • OpenAPI: zod-to-openapi for API docs
  • 50+ integrations on npm

Valibot

  • React Hook Form: @hookform/resolvers/valibot
  • tRPC: Community adapter available
  • Drizzle ORM: drizzle-valibot
  • Growing ecosystem (~15 integrations)

ArkType

  • React Hook Form: Community adapter
  • tRPC: Requires custom adapter
  • Smaller ecosystem (~5 integrations)

Winner: Zod has the most mature ecosystem. If you need specific integrations, verify availability first.

Migration Guide: Switching Between Libraries

From Zod to Valibot

// Zod
z.string().min(1).max(100)

// Valibot
v.pipe(v.string(), v.minLength(1), v.maxLength(100))

// Pattern: method chains → pipe composition
Enter fullscreen mode Exit fullscreen mode

From Zod to ArkType

// Zod
z.object({
  name: z.string(),
  age: z.number().int().positive().optional(),
})

// ArkType
type({
  name: 'string',
  'age?': 'integer>0',
})

// Pattern: schema objects → type strings
Enter fullscreen mode Exit fullscreen mode

Real-World Recommendations

Choose Zod If:

  • You need the largest ecosystem and most integrations
  • Your team values familiarity (most developers know Zod)
  • Performance isn't a critical bottleneck
  • You want the most stable, battle-tested option

Choose Valibot If:

  • Bundle size is critical (edge functions, client-side)
  • You love functional programming patterns
  • You want comparable safety to Zod with better performance
  • You're building a library and want minimal dependencies

Choose ArkType If:

  • Performance is your top priority
  • You want the most expressive type syntax
  • You're comfortable with a newer library
  • You're doing complex type transformations (morphing)

The Verdict: Our Recommendation for 2026

For most projects in 2026, we recommend:

  1. New projects with performance concerns: Valibot
  2. Enterprise/team projects needing stability: Zod
  3. Advanced TypeScript users wanting cutting-edge DX: ArkType

There's no universally "best" choice. Each library makes different tradeoffs:

  • Zod trades performance for ecosystem maturity
  • Valibot trades verbosity for bundle size
  • ArkType trades ecosystem for performance and expressiveness

The good news? All three are excellent, well-maintained, and actively developed. You can't go wrong with any of them—just pick the one that aligns with your priorities.

Conclusion: The Validation Renaissance

We're living in a golden age of TypeScript validation. Zod proved the concept, Valibot optimized it, and ArkType reimagined it. Competition has made all of them better.

If you're starting fresh in 2026:

  • Edge-first or bundle-conscious? → Valibot
  • Safety-first, ecosystem matters? → Zod
  • Performance-first, type wizard? → ArkType

Whatever you choose, you're getting compile-time safety AND runtime validation. That's the real win.


💡 Note: This article was originally published on the Pockit Blog.

Check out Pockit.tools for 60+ free developer utilities. For faster access, add it to Chrome and use JSON Formatter & Diff Checker directly from your toolbar.
Now go validate some data. Type-safely.

Top comments (0)