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 Playground
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(),
  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:

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()),
// });