extern() — Cross-Library Interop
Mix schemas from Zod, Valibot, ArkType, or any Standard Schema v1 library within a single @cleverbrush/schema object.
Why extern()?
The Standard Schema spec lets consumers accept schemas from any library. But what about composing schemas across libraries?
extern() solves this: wrap any Standard Schema v1 compatible schema and use it as a property inside a @cleverbrush/schema object. Types are inferred across the boundary and getErrorsFor selectors work through it too.
This means you can adopt @cleverbrush/schema incrementally — keep your existing Zod, Valibot, or ArkType schemas and compose them into new objects without rewriting anything.
Wrapping a Zod Schema
▶ Open in Playgroundimport { 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(),
zip: z.string().regex(/^\d{5}$/)
});
// Compose it into a @cleverbrush/schema object
const OrderSchema = object({
id: number(),
createdAt: date(),
address: extern(zodAddress),
});
type Order = InferType<typeof OrderSchema>;
// { id: number; createdAt: Date; address: { street: string; city: string; zip: string } }
// Validation runs through both libraries
const result = OrderSchema.validate(data);
if (!result.valid) {
// Typed error access works through the extern() boundary
const zipErrors = result.getErrorsFor(o => o.address.zip);
}Works With Any Standard Schema v1 Library
extern() accepts any object implementing the Standard Schema v1 spec. This includes:
- Zod (v3.24+)
- Valibot (v1+)
- ArkType (v2+)
- And 50+ other libraries
import * as v from 'valibot';
import { object, string, extern, type InferType } from '@cleverbrush/schema';
// Valibot schema
const valibotEmail = v.pipe(v.string(), v.email());
// Mix it into a @cleverbrush/schema object
const ContactSchema = object({
name: string().minLength(2),
email: extern(valibotEmail),
});
type Contact = InferType<typeof ContactSchema>;
// { name: string; email: string }Incremental Migration Strategy
If you have an existing codebase using Zod, you don't need to rewrite everything at once. Use extern() to compose existing schemas into new @cleverbrush/schema objects, then gradually convert individual schemas as needed.
// Phase 1: Wrap existing Zod schemas
const NewFeature = object({
userId: number(),
profile: extern(existingZodProfileSchema), // keep as-is
address: extern(existingZodAddressSchema), // keep as-is
tags: array(string()), // new field, native
});
// Phase 2: Gradually replace extern() calls with native schemas
// const NewFeature = object({
// userId: number(),
// profile: ProfileSchema, // now native
// address: extern(existingZodAddressSchema), // still wrapped
// tags: array(string()),
// });