The cornerstone of type-safe TypeScript

One schema.
Types, validation, forms.

@cleverbrush/schema is an immutable, composable schema library that infers your TypeScript types at compile time and validates your data at runtime — with zero dependencies. It lays the foundation for a rich ecosystem — much like Zod has shown is possible.

Explore the Schema LibraryTry in PlaygroundGitHub
Zero runtime dependenciesCompile-time type inferenceImmutable & composableBSD-3 Licensed~5 KB min (full ~17 KB) gzippedStandard Schema compatible98% test coverageFaster than Zod in most tests

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
# Headless schema-driven React forms (optional)
$npm install @cleverbrush/react-form
# JSON Schema Draft 7 / 2020-12 interop (optional)
$npm install @cleverbrush/schema-json

@cleverbrush/schema and @cleverbrush/mapper have zero runtime dependencies. @cleverbrush/react-form depends only on React and the schema library.

How it compares to Zod

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 in three areas that Zod cannot match.

🎯

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.

// Zod — 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 ✓
🔍

Runtime schema introspection

Both Zod and @cleverbrush/schema let you inspect schema structure at runtime — but @cleverbrush/schema was built with introspection as a first-class concern. Every descriptor returned by .introspect() is fully typed, so you get rich autocomplete and compile-time checks instead of navigating loosely-typed internal properties.

// Zod — possible, but via loosely-typed internals
schema._zod.def.shape.name  // type: any

// @cleverbrush/schema — fully typed descriptor tree
const d = UserSchema.introspect();
d.properties.name.isRequired   // boolean ✓
d.properties.age.isRequired    // boolean ✓
d.properties.email.validators  // TypedValidator[] ✓
🧩

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.

// Zod — only .refine(), no new builder methods

// @cleverbrush/schema — real methods on the builder
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
~5 KBmin bundle (gzip)
faster than Zod in array validation
0runtime dependencies
98%test coverage

@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

~~5 KB gzipped (minimalist build) or ~~17 KBfor 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

Invalid Input

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

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.