Schema Modifiers
Modifiers augment a schema with metadata or behavior without changing its core type. Every modifier returns a new immutable builder instance.
Default Values
▶ Open in PlaygroundEvery schema builder supports .default(value). When the input is undefined, the default value is used instead — and the result is still validated against the schema's constraints.
import { string, number, array, date, object, type InferType } from '@cleverbrush/schema';
// Static default
const Name = string().default('Anonymous');
Name.validate(undefined); // { valid: true, object: 'Anonymous' }
Name.validate('Alice'); // { valid: true, object: 'Alice' }
// Factory function — useful for mutable defaults like arrays or dates
const Tags = array(string()).default(() => []);
// Works with .optional() — .default() removes undefined from the inferred type
const Port = number().optional().default(3000);
type Port = InferType<typeof Port>; // number
// Use factories for mutable values to avoid shared references
const Config = object({
host: string().default('localhost'),
port: number().default(8080),
tags: array(string()).default(() => []),
createdAt: date().default(() => new Date())
});Default values are exposed via .introspect():
const schema = string().default('hello');
const info = schema.introspect();
console.log(info.hasDefault); // true
console.log(info.defaultValue); // 'hello'Catch / Fallback
▶ Open in PlaygroundEvery schema builder supports .catch(value). When validation fails for any reason — wrong type, constraint violation, missing required value — the fallback is returned as a successful result instead of errors.
Unlike .default(), which only fires when the input is undefined, .catch() fires on any validation failure. When .catch() is set, .validate() will never return an invalid result.
import { string, number, array } from '@cleverbrush/schema';
// Static fallback
const Name = string().catch('unknown');
Name.validate(42); // { valid: true, object: 'unknown' }
Name.validate(null); // { valid: true, object: 'unknown' }
Name.validate('Alice'); // { valid: true, object: 'Alice' }
// Constraint violation also triggers catch
const Age = number().min(0).catch(-1);
Age.validate(-5); // { valid: true, object: -1 }
// Factory for mutable fallback values
const Tags = array(string()).catch(() => []);
const r1 = Tags.validate(null); // { valid: true, object: [] }
const r2 = Tags.validate(null); // { valid: true, object: [] }
// r1.object !== r2.object — a fresh [] each time
// Introspectable
const schema = string().catch('unknown');
console.log(schema.introspect().hasCatch); // true
console.log(schema.introspect().catchValue); // 'unknown'Readonly
▶ Open in Playground.readonly() is a type-level-only modifier — it marks the inferred TypeScript type as immutable without altering validation or freezing the value at runtime.
| Builder | Effect on InferType<T> |
|---|---|
object(…).readonly() | Readonly<{ … }> |
array(…).readonly() | ReadonlyArray<T> |
date().readonly() | Readonly<Date> |
| Primitives | Identity — already immutable |
import { object, array, string, number, type InferType } from '@cleverbrush/schema';
const UserSchema = object({ name: string(), age: number() }).readonly();
type User = InferType<typeof UserSchema>;
// Readonly<{ name: string; age: number }>
const TagsSchema = array(string()).readonly();
type Tags = InferType<typeof TagsSchema>;
// ReadonlyArray<string>
console.log(UserSchema.introspect().isReadonly); // trueNote: .readonly() is shallow. For deeply nested immutability, apply it at each level.
Describe
▶ Open in Playground.describe(text) attaches a human-readable description as metadata only — no effect on validation. The description is accessible via .introspect().description and is automatically emitted by toJsonSchema().
import { object, string, number } from '@cleverbrush/schema';
import { toJsonSchema } from '@cleverbrush/schema-json';
const ProductSchema = object({
id: string().uuid().describe('Unique product identifier'),
name: string().nonempty().describe('Display name shown to customers'),
price: number().positive().describe('Price in USD')
}).describe('A product in the catalogue');
// Read at runtime
console.log(ProductSchema.introspect().description);
// 'A product in the catalogue'
// toJsonSchema emits description fields automatically
const schema = toJsonSchema(ProductSchema, { $schema: false });
// { type: 'object', description: 'A product in the catalogue', properties: { … } }schemaName
.schemaName(name) attaches a component name for OpenAPI tooling. When used with @cleverbrush/server-openapi, schemas with a name are automatically extracted into components/schemas and referenced via $ref.
import { object, string, number } from '@cleverbrush/schema';
const UserSchema = object({
id: number(),
name: string().nonempty(),
}).schemaName('User');
console.log(UserSchema.introspect().schemaName); // 'User'
// In the generated OpenAPI spec:
// { "$ref": "#/components/schemas/User" }Promise Schemas
Use promise() to validate that a value is a Promise (or any thenable). Pass an optional schema to annotate the resolved value type.
import { promise, string, number, object, InferType } from '@cleverbrush/schema';
// Untyped — accepts any Promise
const schema = promise();
type Result = InferType<typeof schema>; // Promise<any>
// Typed — constrains the resolved value
const userPromise = promise(
object({ id: number(), name: string() })
);
type UserPromise = InferType<typeof userPromise>;
// Promise<{ id: number; name: string }>
// Set/replace resolved type incrementally
const refined = promise()
.hasResolvedType(number())
.optional()
.default(Promise.resolve(0));