Skip to content

Schema Libraries ​

itty-spec uses the Standard Schema V1 interface, which provides a common abstraction layer for schema validation. This means you can use any Standard Schema V1 compatible library.

Supported Libraries ​

Zod v4 is fully supported with excellent TypeScript inference and OpenAPI generation.

Installation ​

bash
npm install zod@v4

Basic Usage ​

ts
import { z } from "zod";
import { createContract } from "itty-spec";

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  age: z.number().min(18).optional(),
});

const contract = createContract({
  getUser: {
    path: "/users/:id",
    method: "GET",
    pathParams: z.object({
      id: z.string().uuid(),
    }),
    responses: {
      200: {
        "application/json": { body: UserSchema },
      },
    },
  },
});

Zod Features ​

  • Excellent TypeScript inference
  • Rich validation methods
  • Transform support
  • Refinement support
  • OpenAPI generation support

Example with Transforms ​

ts
const QuerySchema = z.object({
  page: z.string().transform(Number).pipe(z.number().min(1)),
  limit: z.string().transform(Number).pipe(z.number().min(1).max(100)),
  tags: z.string().transform(s => s.split(',')),
});

Valibot ​

Valibot is fully supported with OpenAPI generation via @standard-community/standard-openapi.

Installation ​

bash
npm install valibot

Basic Usage ​

ts
import * as v from "valibot";
import { createContract } from "itty-spec";

const UserSchema = v.object({
  id: v.pipe(v.string(), v.uuid()),
  name: v.pipe(v.string(), v.minLength(1)),
  email: v.pipe(v.string(), v.email()),
  age: v.optional(v.pipe(v.number(), v.minValue(18))),
});

const contract = createContract({
  getUser: {
    path: "/users/:id",
    method: "GET",
    pathParams: v.object({
      id: v.pipe(v.string(), v.uuid()),
    }),
    responses: {
      200: {
        "application/json": { body: UserSchema },
      },
    },
  },
});

Valibot Features ​

  • Smaller bundle size than Zod
  • Similar API to Zod
  • OpenAPI generation support
  • Good TypeScript inference

Example with Pipes ​

ts
const QuerySchema = v.object({
  page: v.pipe(
    v.string(),
    v.transform(Number),
    v.number(),
    v.minValue(1)
  ),
  limit: v.pipe(
    v.string(),
    v.transform(Number),
    v.number(),
    v.minValue(1),
    v.maxValue(100)
  ),
});

Standard Schema V1 ​

Standard Schema V1 provides a common interface that all compatible libraries implement. This ensures:

  • Portability: Switch between libraries without changing your contracts
  • Type Safety: Consistent type inference across libraries
  • Runtime Validation: Same validation behavior regardless of library

Standard Schema Interface ​

All Standard Schema V1 compatible schemas implement:

ts
interface StandardSchemaV1 {
  '~standard': {
    validate: (data: unknown) => Promise<ValidationResult>;
  };
}

Choosing a Schema Library ​

Use Zod if: ​

  • You want the best TypeScript inference
  • You need extensive validation features
  • You prefer a more mature ecosystem
  • Bundle size is not a primary concern

Use Valibot if: ​

  • Bundle size is critical
  • You want similar features to Zod
  • You're building for edge/serverless environments
  • You prefer a more modular approach

Migration Between Libraries ​

Since both libraries implement Standard Schema V1, you can migrate between them:

From Zod to Valibot ​

ts
// Before (Zod)
import { z } from "zod";

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
});

// After (Valibot)
import * as v from "valibot";

const UserSchema = v.object({
  id: v.pipe(v.string(), v.uuid()),
  name: v.pipe(v.string(), v.minLength(1)),
  email: v.pipe(v.string(), v.email()),
});

From Valibot to Zod ​

ts
// Before (Valibot)
import * as v from "valibot";

const UserSchema = v.object({
  id: v.pipe(v.string(), v.uuid()),
  name: v.pipe(v.string(), v.minLength(1)),
  email: v.pipe(v.string(), v.email()),
});

// After (Zod)
import { z } from "zod";

const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
});

Library-Specific Patterns ​

Zod Patterns ​

Using Refinements ​

ts
const PasswordSchema = z.string()
  .min(8)
  .refine(
    (val) => /[A-Z]/.test(val),
    { message: "Password must contain at least one uppercase letter" }
  )
  .refine(
    (val) => /[0-9]/.test(val),
    { message: "Password must contain at least one number" }
  );

Using Preprocess ​

ts
const QuerySchema = z.preprocess(
  (val) => {
    if (typeof val === 'string') {
      return { page: val, limit: '10' };
    }
    return val;
  },
  z.object({
    page: z.string(),
    limit: z.string(),
  })
);

Valibot Patterns ​

Using Pipes ​

ts
const PasswordSchema = v.pipe(
  v.string(),
  v.minLength(8),
  v.custom((val) => /[A-Z]/.test(val), "Must contain uppercase"),
  v.custom((val) => /[0-9]/.test(val), "Must contain number")
);

Using Transform ​

ts
const QuerySchema = v.object({
  page: v.pipe(
    v.string(),
    v.transform(Number),
    v.number(),
    v.minValue(1)
  ),
});

OpenAPI Generation ​

Both Zod and Valibot support OpenAPI generation:

Zod OpenAPI ​

Zod schemas are automatically converted to OpenAPI:

ts
import { createOpenApiSpecification } from "itty-spec/openapi";

const spec = await createOpenApiSpecification(contract, {
  title: "My API",
  version: "1.0.0",
});

Valibot OpenAPI ​

Valibot schemas are converted via @standard-community/standard-openapi:

ts
import { createOpenApiSpecification } from "itty-spec/openapi";

// Works the same way
const spec = await createOpenApiSpecification(contract, {
  title: "My API",
  version: "1.0.0",
});

Best Practices ​

1. Be Consistent ​

Use the same library throughout your project:

ts
// ✅ Good - consistent
import { z } from "zod";
// Use Zod everywhere

// ❌ Bad - mixed
import { z } from "zod";
import * as v from "valibot";
// Using both libraries

2. Reuse Schemas ​

Define schemas once and reuse:

ts
// ✅ Good
const UserSchema = z.object({ /* ... */ });

const contract = createContract({
  getUser: {
    // Uses UserSchema
    responses: { 200: { "application/json": { body: UserSchema } } },
  },
  createUser: {
    // Reuses UserSchema
    responses: { 201: { "application/json": { body: UserSchema } } },
  },
});

3. Use Type Inference ​

Let TypeScript infer types from schemas:

ts
// ✅ Good
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
});

type User = z.infer<typeof UserSchema>;

// ❌ Bad - manually typing
type User = {
  id: string;
  name: string;
};

4. Validate Early ​

Define schemas for all inputs:

ts
// ✅ Good - validates everything
const contract = createContract({
  getUser: {
    path: "/users/:id",
    pathParams: z.object({ id: z.string().uuid() }),
    query: z.object({ include: z.array(z.string()).optional() }),
    headers: z.object({ authorization: z.string() }),
    responses: { /* ... */ },
  },
});

Released under the MIT License.