The cornerstone of type-safe TypeScript

One schema for types

@cleverbrush/schema infers TypeScript types at compile time, validates untrusted data at runtime, and exposes typed descriptors that power forms, mappers, OpenAPI, and your own tooling.

Immutable buildersStandard Schema compatibleTyped field selectorsBSD-3 licensed
schema.ts
4.1 KBminimal gzipped build
17.7 KBfull gzipped build
0runtime dependencies
0%line coverage
runtime validationcompile-time inferenceproperty descriptorsschema-driven formsJSON Schema interoptyped mappers

Get started in seconds

Install only what you need — each package is independent and tree-shakeable.

# Schema — validation + TypeScript inference
$npm install @cleverbrush/schema
# Type-safe object mapping (optional)
$npm install @cleverbrush/mapper
# JSON Schema Draft 7 / 2020-12 interop (optional)
$npm install @cleverbrush/schema-json

@cleverbrush/schema and @cleverbrush/mapper have zero runtime dependencies.

What makes it different

If you know Zod, you already know most of the API — the primitives, the fluent builder style, and InferType all work the same way. @cleverbrush/schema goes further with three capabilities no other schema library offers.

🎯

Typed field-error selectors

Access per-field errors through a typed lambda — no magic strings, no brittle path navigation. TypeScript catches a misspelled field name at compile time.

// String path — no compile-time check
result.error?.issues.filter(i => i.path[0] === 'naem') // ← typo silently passes

// @cleverbrush/schema — typed selector
result.getErrorsFor(u => u.naem) // ← TypeScript error ✓
🧩

Type-safe extension system

Add your own methods to schema builders — fully typed, autocomplete-ready. The built-in .email(), .url(), .uuid() methods use the same public API.

const slugExt = defineExtension({
  string: {
    slug(this: StringSchemaBuilder) {
      return this.matches(/^[a-z0-9-]+$/);
    }
  }
});
const { string: s } = withExtensions(slugExt);
const PostSlug = s().slug().minLength(3);
//               ^ slug() is typed, autocomplete works
🔍

Runtime introspection & PropertyDescriptors

Every schema exposes a typed .introspect() descriptor tree. This powers the mapper, react-form, JSON Schema output — and your own tools. Navigate fields with typed lambdas, not strings.

const d = UserSchema.introspect();
d.properties.name.isRequired   // boolean ✓
d.properties.age.validators    // TypedValidator[] ✓

// PropertyDescriptors — typed field navigation
const desc = UserSchema.properties;
desc(u => u.address.city).getValue(data) // typed, refactor-safe

@cleverbrush/schema

The foundation everything else builds on

Runtime validation

Validate untrusted input at API boundaries, form submissions, or config files — with detailed error messages.

🔷TypeScript inference

The TypeScript type is derived automatically from the schema. No duplicate interface declarations.

🧱Immutable & composable

Every builder call returns a new instance. Schemas are safe to share across modules without side effects.

📦Zero dependencies

~4.1 KB gzipped (minimalist build) or ~17.7 KB for the full build. Runs in Node, Deno, Bun, and modern browsers.

🔗Standard Schema & Zod interop

Implements the Standard Schema spec, so it works alongside any compatible library out of the box — including Zod.

🔍Typed field-error selectors

After validation, look up errors for a specific field using an arrow function — result.getErrorsFor(u => u.email)— instead of a plain string like errors['email']. TypeScript verifies the property exists at compile time, so a typo like u => u.emal is a build error, not a runtime surprise.

import { object, string, number, type InferType } from '@cleverbrush/schema';

// ── 1. Define once, get the type for free ───────────────────────────
const UserSchema = object({
  name:  string().minLength(2, 'Name must be at least 2 characters'),
  email: string().minLength(5, 'Please enter a valid email'),
  age:   number().min(0).max(150)
});

type User = InferType<typeof UserSchema>;
// → { name: string; email: string; age: number }

// ── 2. Validate and read field-level errors with typed selectors ──────
const result = UserSchema.validate(rawInput);
if (!result.valid) {
  // getErrorsFor is a method on the result — no magic strings
  const nameErrors = result.getErrorsFor(u => u.name);
  console.log(nameErrors.errors); // ['Name must be at least 2 characters']
}

What the schema enables

@cleverbrush/schema's runtime introspection powers a family of companion libraries. Define your data shape once and get type-safe mapping, JSON Schema interop, and headless React forms — all from the same schema object.

@cleverbrush/mapperType-safe object mapping between two schemas. Compile-time completeness — the compiler errors if you forget a property.
@cleverbrush/react-formHeadless, schema-driven React forms. Works with Material UI, Ant Design, or plain HTML inputs.
@cleverbrush/schema-jsonBidirectional JSON Schema (Draft 7 / 2020-12) interop — convert to and from typed schema builders.

Familiar API — migrate from Zod field by field

Most Zod primitives are drop-in replacements. The fluent builder style, optional(), default(), brand(), and readonly() all work identically. You can adopt @cleverbrush/schema incrementally — even wrapping existing Zod schemas with extern() to compose them into new objects.

// ── Zod ───────────────────────────────────────────────────────────
import { z } from 'zod';
const UserZ = z.object({
  name:  z.string().min(2),
  email: z.string().min(5),
  age:   z.number().min(0).max(150),
});
type UserZ = z.infer<typeof UserZ>; // requires z.infer<>

// ── @cleverbrush/schema ──────────────────────────────────────────────
import { object, string, number, type InferType } from '@cleverbrush/schema';
const UserSchema = object({
  name:  string().minLength(2),
  email: string().minLength(5),
  age:   number().min(0).max(150),
});
type User = InferType<typeof UserSchema>; // { name: string; email: string; age: number } — identical to z.infer<typeof UserZ>

// Validate — result.valid narrows the type automatically
const result = UserSchema.validate(rawInput);
if (result.valid) {
  processUser(result.object); // typed as User ✓
}
▶ Try this example in the Playground

Immutable & composable — share schemas safely

Every builder method returns a new instance. Schemas can be exported, extended, or narrowed anywhere in your codebase without risk of accidental mutation.

import { object, string, number, union, boolean, array, type InferType } from '@cleverbrush/schema';

// Reusable building blocks
const EmailField = string().minLength(5).maxLength(254);
const NameField  = string().minLength(2).maxLength(50);

// Extend a base schema for two contexts — neither mutates the other
const CreateUserSchema = object({ name: NameField, email: EmailField });
const UpdateUserSchema = CreateUserSchema
  .addProps({ role: string().optional() });

// Discriminated unions with full type narrowing
const MediaSchema = union(object({ type: string().equals('image'), url: string(), width: number(), height: number() }))
  .or(object({ type: string().equals('video'), url: string(), duration: number() }))
  .or(object({ type: string().equals('text'),  body: string() }));
type Media = InferType<typeof MediaSchema>;
// { type: 'image'; url: string; width: number; height: number }
// | { type: 'video'; url: string; duration: number }
// | { type: 'text';  body: string }
▶ Try this example in the Playground

Advanced: adopt incrementally with extern()

Already invested in Zod (or any Standard Schema v1 compatible library)? Wrap existing schemas with extern() and use them as properties inside @cleverbrush/schema objects. Types are inferred across the boundary and getErrorsFor selectors work through it too.

import { z } from 'zod';
import { object, date, number, extern, type InferType } from '@cleverbrush/schema';

// Your existing Zod schema — untouched
const zodAddress = z.object({ street: z.string(), city: z.string() });

// Compose it into a @cleverbrush/schema object
const OrderSchema = object({
  id:        number(),
  createdAt: date(),
  address:   extern(zodAddress),  // ← any Standard Schema v1 compatible library
});

type Order = InferType<typeof OrderSchema>;
// { id: number; createdAt: Date; address: { street: string; city: string } }
▶ Try extern() in the Playground

Thoroughly Tested

Every package ships with a comprehensive unit test suite run with Vitest across the full monorepo. Coverage is measured and published on every release.

98.4%Line coverage
98.7%Function coverage
92.6%Branch coverage
View source & tests on GitHub →

Performance

Numbers generated automatically from our benchmark suite (Vitest bench, Node >= 22). Higher is better. We show all tests— including the ones where we don't come first.

Valid Input

String (valid)

1schema
9.6M ops/s
2zod
8.1M ops/s
3joi
3.9M ops/s
4yup
1.2M ops/s

Flat object (valid)

1schema
2.1M ops/s
2zod
1.7M ops/s
3joi
436K ops/s
4yup
219K ops/s

Nested object (valid)

1schema
1.4M ops/s
2zod
935K ops/s
3joi
283K ops/s
4yup
119K ops/s

Complex object (valid)

1schema
460K ops/s
2zod
348K ops/s
3joi
103K ops/s
4yup
32K ops/s

Array of 100 (valid)

1schema
75K ops/s
2zod
44K ops/s
3joi
14K ops/s
4yup
3K ops/s

Union first (valid)

1schema
4.3M ops/s
2zod
3.2M ops/s
3joi
1.3M ops/s
4yup
341K ops/s

Union last (valid)

1zod
1.5M ops/s
2schema
1.5M ops/s
3joi
473K ops/s
4yup
28K ops/s

Zod wins this one.We show all results — including the ones we don't top.

Invalid Input

String (invalid)

1schema
10.8M ops/s
2zod
1.5M ops/s
3joi
822K ops/s
4yup
77K ops/s

Flat object (invalid)

1schema
5.5M ops/s
2joi
822K ops/s
3zod
457K ops/s
4yup
63K ops/s

Nested object (invalid)

1schema
5.7M ops/s
2joi
807K ops/s
3zod
263K ops/s
4yup
49K ops/s

Complex object (invalid)

1schema
2.4M ops/s
2joi
513K ops/s
3zod
88K ops/s
4yup
37K ops/s

Array of 100 (invalid)

1schema
3.3M ops/s
2joi
536K ops/s
3zod
17K ops/s
4yup
9K ops/s

Union no-match (invalid)

1schema
11.7M ops/s
2zod
1.2M ops/s
3joi
163K ops/s
4yup
17K ops/s

Benchmarks compare @cleverbrush/schema, Zod, Yup, and Joi. Each test runs for 500 ms per library. Full source in libs/benchmarks/.

Building a web server?

The same schemas power a full web framework — type-safe server endpoints, auto-typed clients, OpenAPI spec generation, auth, and DI — all derived from one schema definition. Zero duplication, zero type drift.

Help us build the future of typed web development

Cleverbrush libraries are open source and community-driven. Whether it's fixing a bug, improving docs, suggesting a feature, or building a new library — every contribution makes the ecosystem stronger for everyone.