LabHub

Blog

TypeScript Data Validation 2026 — Zod·Valibot·ArkType·TypeBox·Effect Schema Deep Comparison (Runtime Validation in the AI Tool-Calling Era)

한국어English日本語

Prologue — The era when every API boundary demands a schema

In 2022, "what should we use for TypeScript runtime validation?" had one answer: Zod. Before that, Joi, Yup, or hand-written type guards.

In 2026 the same question has five candidates. A lot happened in between.

Five camps emerged.

This post compares these libraries head-to-head as of May 2026. Seven axes: bundle, inference, JSON Schema, async, OpenAPI, runtime cost, ecosystem. Plus the same User schema in all five.


1. Landscape — Map of data validation libraries

Classification first. Not everything sits in the same place.

CategoryRepresentativeOne-line summary
Object builder (chaining)Zod, Yup, Joiz.string().email() style. Most familiar.
Modular functionsValibotstring([email()]). Tree-shake friendly.
TS syntax as schemaArkTypeUse TS type expressions at runtime.
JSON Schema nativeTypeBoxWhat you write is JSON Schema.
Functional + EffectEffect SchemaTyped errors, bidirectional encoding.
Lightweight assertionSuperstruct, io-tsSmall, composable, slightly older style.

This post focuses on the five bold splits — Zod, Valibot, ArkType, TypeBox, Effect Schema. Yup, Joi, and Superstruct get a short chapter near the end.

Why these five


2. Seven-axis comparison matrix

Before the deep dives, here's the big picture.

AxisZod 4ValibotArkTypeTypeBoxEffect Schema
Bundle (core)MediumVery small (modular)SmallVery smallLarge (Effect core included)
TS inference qualityVery goodVery goodBest (TS expressions as-is)GoodVery good
JSON Schema exportExternal pkgExternal pkgBuilt-inBuilt-in (definition IS JSON Schema)Built-in
Async validationYesYesPartialNo (sync-focused)Yes
OpenAPI exportzod-to-openapi@valibot/to-json-schemaBuilt-inBuilt-in (Fastify/AJV)@effect/schema-openapi
Runtime costFast (large gain in v4)Very fastFastFastest (when AJV-compiled)Medium–fast
EcosystemLargestGrowing fastMediumAPI/Fastify campEffect users

Don't decide from this table alone. Next chapters pin down what each does and doesn't do.


3. Zod — Why it became the standard, and Zod 4's comeback

Since landing in 2020, Zod has been the de-facto TypeScript validation standard. Three reasons.

  1. Chaining API familiarity. z.string().email().min(5) reads as intent. Near-zero learning cost for anyone coming from Joi or Yup.
  2. z.infer type inference. Derive TS types directly from the schema. Schema = single source of truth for the type.
  3. Ecosystem explosion. tRPC, @hookform/resolvers, OpenAI SDK, LangChain, Next.js Server Actions — all accept Zod first-class.

Zod 3-era weaknesses

Two criticisms followed Zod for years.

What Zod 4 changed

Zod 4 beta arrived October 2024; GA followed in 2025. Three big changes.

Same User schema in Zod

import { z } from 'zod'

const User = z.object({
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
  name: z.string().min(1),
})

type User = z.infer<typeof User>

const parsed = User.parse({ email: 'foo@example.com', age: 30, name: 'Foo' })
//    ^ parsed: User

safeParse for errors as values:

const result = User.safeParse(unknown)
if (!result.success) {
  // result.error is a ZodError; pretty issues at result.error.issues
  console.error(result.error.flatten())
}

Zod strengths (2026)

Zod weaknesses (2026)


4. Valibot — Shrinking the bundle with functional modules

Valibot exploded between late 2023 and 2024. Core philosophy:

"Write the basics as functions. What you don't use isn't in the bundle."

Where Zod is z.string().email().min(5), Valibot is function composition.

import * as v from 'valibot'

const User = v.object({
  email: v.pipe(v.string(), v.email()),
  age: v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(150)),
  name: v.pipe(v.string(), v.minLength(1)),
})

type User = v.InferOutput<typeof User>

const parsed = v.parse(User, { email: 'foo@example.com', age: 30, name: 'Foo' })

Compose validators with v.pipe(...), then apply with v.parse/v.safeParse. Functions are all named exports — esbuild/Rollup drops unused ones.

Bundle gap

The picture from the official site and community benchmarks:

Valibot's BaseSchema API

Stable since Valibot 1.0 GA in 2024. Main functions:

Async uses v.parseAsync/v.safeParseAsync.

Valibot strengths

Valibot weaknesses


5. ArkType — TypeScript syntax, executable at runtime

ArkType drew attention with its v2 GA in 2024. Different premise:

"Take a TypeScript expression as a string and turn it into a runtime schema."

Same User schema:

import { type } from 'arktype'

const User = type({
  email: 'string.email',
  age: 'number.integer >= 0',
  name: 'string > 0',
})

// type inference: TS understands the expression precisely
type User = typeof User.infer

const out = User({ email: 'foo@example.com', age: 30, name: 'Foo' })
if (out instanceof type.errors) {
  console.error(out.summary)
} else {
  // out is the validated value
}

The trick: an expression like 'number.integer >= 0' is parsed by TS template literal types into a precise type. ArkType defines a mini DSL, parses it at the type level, and produces a schema whose inferred type matches a hand-written TS type 1:1.

What ArkType does differently

Object form

You can also pass a TS object directly:

import { type } from 'arktype'

const Post = type({
  id: 'number.integer',
  title: 'string > 0',
  tags: 'string[]',
  author: {
    id: 'number',
    name: 'string',
  },
  publishedAt: 'Date',
})

ArkType strengths

ArkType weaknesses


6. TypeBox — What you write is JSON Schema

TypeBox emerged in 2021, was loved by the Fastify camp, and got a second wind in the AI tool-calling era.

"My schema is literally JSON Schema. Compile with AJV and you've got the fastest runtime."

Same User schema:

import { Type, Static } from '@sinclair/typebox'
import { Value } from '@sinclair/typebox/value'

const User = Type.Object({
  email: Type.String({ format: 'email' }),
  age: Type.Integer({ minimum: 0, maximum: 150 }),
  name: Type.String({ minLength: 1 }),
})

type User = Static<typeof User>

const ok = Value.Check(User, unknown)
const errors = [...Value.Errors(User, unknown)]

What Type.Object({ ... }) returns IS a JSON Schema document. JSON.stringify(User) gives a standard JSON Schema.

Pairing with AJV — fastest runtime

import Ajv from 'ajv'
import addFormats from 'ajv-formats'
const ajv = addFormats(new Ajv())

const check = ajv.compile(User)
const ok = check(value) // compiled function — very fast

AJV compiles JSON Schema into V8-friendly code. The result is generally the fastest runtime validator in this category.

TypeBox + Fastify

import Fastify from 'fastify'

const app = Fastify().withTypeProvider<TypeBoxTypeProvider>()

app.post('/users', {
  schema: { body: User, response: { 200: User } },
}, async (req) => {
  // req.body is typed as User automatically
  return req.body
})

The schema doubles as OpenAPI response spec, runtime validator, and response serializer. One definition, four jobs.

TypeBox for AI tool calling

OpenAI, Anthropic, and Gemini function/tool definitions are JSON Schema.

const GetWeather = Type.Object({
  city: Type.String(),
  units: Type.Optional(Type.Union([Type.Literal('c'), Type.Literal('f')])),
})

const tool = {
  type: 'function',
  function: {
    name: 'get_weather',
    description: 'Current weather for a given city',
    parameters: GetWeather, // already JSON Schema
  },
}

No conversion via zod-to-json-schema. Nothing lost in translation (format, description, etc.).

TypeBox strengths

TypeBox weaknesses


7. Effect Schema — The Effect ecosystem's validator

Effect is a Scala/Haskell-influenced full-stack TS functional framework. Effect Schema is its data validation/transform module.

"Validation is bidirectional transformation. And every error belongs in the type."

Same User schema:

import { Schema } from 'effect'

const User = Schema.Struct({
  email: Schema.String.pipe(Schema.email()),
  age: Schema.Number.pipe(Schema.int(), Schema.between(0, 150)),
  name: Schema.NonEmptyString,
})

type User = Schema.Schema.Type<typeof User>

const decode = Schema.decodeUnknownSync(User)
const out = decode({ email: 'foo@example.com', age: 30, name: 'Foo' })

decodeUnknownSync is the sync entry. decodeUnknown/decode returns an Effect value — composable inside the Effect stack.

What Effect Schema does differently

Bidirectional transform example

import { Schema } from 'effect'

// Convert ISO string -> domain Date both ways
const DateFromString = Schema.transform(Schema.String, Schema.Date, {
  decode: (s) => new Date(s),
  encode: (d) => d.toISOString(),
})

const Event = Schema.Struct({
  name: Schema.NonEmptyString,
  at: DateFromString,
})

const decoded = Schema.decodeUnknownSync(Event)({ name: 'Launch', at: '2026-05-14T00:00:00Z' })
//      ^ decoded.at is a real Date

Effect Schema strengths

Effect Schema weaknesses


8. Same schema, five libraries — User(email + age + name)

Five side by side. Same semantics.

8.1 Zod

import { z } from 'zod'

const User = z.object({
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
  name: z.string().min(1),
})

type User = z.infer<typeof User>

8.2 Valibot

import * as v from 'valibot'

const User = v.object({
  email: v.pipe(v.string(), v.email()),
  age: v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(150)),
  name: v.pipe(v.string(), v.minLength(1)),
})

type User = v.InferOutput<typeof User>

8.3 ArkType

import { type } from 'arktype'

const User = type({
  email: 'string.email',
  age: '0 <= number.integer <= 150',
  name: 'string > 0',
})

type User = typeof User.infer

8.4 TypeBox

import { Type, Static } from '@sinclair/typebox'

const User = Type.Object({
  email: Type.String({ format: 'email' }),
  age: Type.Integer({ minimum: 0, maximum: 150 }),
  name: Type.String({ minLength: 1 }),
})

type User = Static<typeof User>

8.5 Effect Schema

import { Schema } from 'effect'

const User = Schema.Struct({
  email: Schema.String.pipe(Schema.email()),
  age: Schema.Number.pipe(Schema.int(), Schema.between(0, 150)),
  name: Schema.NonEmptyString,
})

type User = Schema.Schema.Type<typeof User>

Comparison — same schema, different philosophies

LibraryLinesStyleInference call
ZodShortChainingz.infer
ValibotMediumFunction composition (pipe)v.InferOutput
ArkTypeShortestTS DSL stringtypeof X.infer
TypeBoxMediumType builderStatic
Effect SchemaMedium.pipe chainingSchema.Schema.Type

Zod and ArkType win readability. Valibot and TypeBox win bundle and tree-shaking. Effect Schema wins when you need transforms and typed errors.


9. The AI tool-calling era — JSON Schema is mandatory

The biggest trend in 2024–2025: every major LLM accepts tool definitions as JSON Schema.

9.1 OpenAI function calling

const tools = [{
  type: 'function',
  function: {
    name: 'get_weather',
    description: 'Get the current weather in a given city',
    parameters: {
      type: 'object',
      properties: {
        city: { type: 'string' },
        units: { type: 'string', enum: ['c', 'f'] },
      },
      required: ['city'],
    },
  },
}]

parameters is standard JSON Schema. Either write it raw or convert from a TS schema library.

9.2 Anthropic tool use

const tools = [{
  name: 'get_weather',
  description: 'Get the current weather in a given city',
  input_schema: {
    type: 'object',
    properties: {
      city: { type: 'string' },
      units: { type: 'string', enum: ['c', 'f'] },
    },
    required: ['city'],
  },
}]

Only the key name differs (input_schema). Shape is identical.

9.3 JSON Schema conversion per library

9.4 What gets lost in conversion

JSON Schema is narrower than TS expressions. Conversion frequently drops:

9.5 OpenAI structured outputs and strict mode

The structured outputs feature OpenAI launched in late 2024 enforces a JSON Schema subset (e.g., additionalProperties: false, required fields, no $ref, etc.). If your TS library's emitted JSON Schema breaks those rules, OpenAI rejects it.

Practical tips:


10. Seven-axis deep comparison

10.1 Bundle size

Roughly, with a small schema usage profile.

LibrarySmall usageLarge usageNotes
ValibotSmallest (hundreds of B)SmallPinnacle of tree-shaking
TypeBoxVery smallSmallType.* is itself light
ArkTypeSmallMediumParser ships with it
Zod 4Medium (big drop)Mediumv4-mini even smaller
Zod 3LargeLargeWeak tree-shaking
Effect SchemaLargeLargeIncludes Effect core
YupMediumMedium
JoiLargeLargeLow browser affinity

10.2 TS inference quality

LibraryInferenceNotes
ArkTypeBestTS expressions as-is
Zod 4Very goodz.infer everywhere
ValibotVery goodInferOutput/InferInput
Effect SchemaVery goodIncludes bidirectional types
TypeBoxGoodStatic
YupFairInference is weak
JoiWeakHand-write types

10.3 JSON Schema export

LibrarySupportHow
TypeBoxNativeDefinition itself
ArkTypeBuilt-in.toJsonSchema()
Effect SchemaBuilt-inJSONSchema.make()
ValibotOfficial external@valibot/to-json-schema
ZodExternalzod-to-json-schema, @asteasolutions/zod-to-openapi
YupThinUnofficial converters
JoiNoneWrite by hand

10.4 Async validation

LibrarySupportPattern
ZodStrongz.string().refine(async ...) + parseAsync
ValibotStrongparseAsync/safeParseAsync
Effect SchemaStrongEffect is async by nature
ArkTypePartialSync-focused
TypeBoxWeakSync-only design

10.5 OpenAPI export

LibrarySupportTooling
TypeBoxVery goodFastify + Type Provider
ArkTypeGood.toJsonSchema() direct
ZodGood@asteasolutions/zod-to-openapi, @hono/zod-openapi
Effect SchemaGoodEffect HTTP + OpenAPI adapters
ValibotGood@valibot/to-json-schema

10.6 Runtime cost (rough, hot-path)

LibraryValidation speedNotes
TypeBox + AJV (compile)FastestV8-friendly compiled code
ValibotVery fastSimple function composition
ArkTypeFastPre-compiled validators
Zod 4FastBig improvement
Effect SchemaMedium–fastEffect overhead
Zod 3MediumWeak on deep objects
JoiSlowHeavy older API

(Numbers depend on the case. Use each library's official benchmarks and measure on your own workload.)

10.7 Ecosystem (adapters and integrations)

LibrarytRPCRHFOpenAI/Anthropic SDKFastifyHonoNext.js
ZodFirst-classFirst-classFirst-classOKOKFirst-class
ValibotSupportedFirst-class (@hookform/resolvers/valibot)External convertOKOKOK
ArkTypeSupportedSupportedExternal convertOKOKOK
TypeBoxPartialPartialDirectFirst-classFirst-class (@hono/typebox-validator)OK
Effect SchemaPartialPartialExternal convertOKOKOK

11. Yup, Joi, Superstruct — Alive but…

11.1 Yup

Standard from the Formik era. Still common in form validation but new adoption is shrinking. Weaker TS inference and heavier bundle than Zod/Valibot.

import * as yup from 'yup'

const User = yup.object({
  email: yup.string().email().required(),
  age: yup.number().integer().min(0).max(150).required(),
  name: yup.string().min(1).required(),
})

11.2 Joi

The old Node backend standard. Low browser affinity (large bundle, deep deps). Still very present in legacy server codebases.

const Joi = require('joi')
const User = Joi.object({
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(0).max(150).required(),
  name: Joi.string().min(1).required(),
})

11.3 Superstruct

Small, lightweight validation. Friendlier than io-ts but a smaller ecosystem than Zod/Valibot. A solid pick for small projects and internal library validation.

import { object, string, number, refine } from 'superstruct'

const Email = refine(string(), 'email', (v) => /.+@.+/.test(v))
const User = object({
  email: Email,
  age: number(),
  name: string(),
})

11.4 io-ts

The traditional functional camp. Effect Schema is rapidly taking that seat. Few new projects start with io-ts voluntarily.


12. Standard Schema — Cross-library compatibility

Around late 2024 to 2025 a Standard Schema spec emerged — the minimum interface validation libraries agreed on. The point: let tools like tRPC, Hono, and React Hook Form accept Zod/Valibot/ArkType through a single adapter.

Core interface (simplified):

interface StandardSchemaV1<Input = unknown, Output = Input> {
  '~standard': {
    version: 1
    vendor: string
    validate: (value: unknown) => StandardSchemaV1.Result<Output> | Promise<StandardSchemaV1.Result<Output>>
    types?: { input: Input; output: Output }
  }
}

Zod, Valibot, ArkType, and Effect Schema all implement this. The upshot — library-switching cost dropped. Coexisting libraries in one codebase, or migrating between them, is cheaper than before.

That said, Standard Schema is the minimum common denominator. Library-specific features (transform, refine, brand…) still need explicit adapter work.


13. What to pick when — An honest verdict

The honest answer is, as always, "it depends." But patterns exist.

Pick Zod 4 when

Pick Valibot when

Pick ArkType when

Pick TypeBox when

Pick Effect Schema when

Pick Yup, Joi, Superstruct when

Anti-checklist (patterns to avoid)


Epilogue — Schemas are the truth at the API boundary

Picking a validation library is a tradeoff between how small, how familiar, how far you can take it. No single winner. But the May 2026 landscape is clear.

Decision checklist

Anti-patterns

  1. Switching libraries without measuring.
  2. Sending the conversion result to AI tools without inspecting it — missing additionalProperties/required happens often.
  3. Putting core validation in refine that JSON Schema can't represent — disappears in conversion.
  4. TypeBox for forms — RHF adapter is weak. Zod/Valibot are natural.
  5. Repeating z.parse on the same large object per request — for hot paths consider compiled validators (TypeBox + AJV).
  6. Two sources of truth for one schema — DB shape and validation schema defined in different places and drifting.
  7. Skipping error-message customization — library default messages leak to users.

Coming up next

Candidates: Debugging AI tool-calling JSON Schema — common OpenAI structured outputs and Anthropic tool use mistakes, One month migrating a 50k-line codebase to Zod 4, Valibot deep dive — what the tree-shaker actually drops.

"Validation is the boundary of code. Code with clean boundaries travels far."

— TypeScript Data Validation 2026, end.


References

Comments

No comments yet.

Sign in to leave a comment