4.1 KiB
schema-registry
(no submodules)
Purpose
Instance-based registry for reusable Zod schemas. Each schema is bound
to a stable name and may be derived into related shapes via
.extend/.partial/.pick/.omit. The registry is deliberately
instance-only — there is no module-level singleton — so multi-tenant
servers and parallel tests cannot accidentally share state. Calls into
the derivation helpers raise typed OpenAPIErrors when the base name
is unknown or refers to a non-ZodObject schema, replacing upstream's
silent undefined return.
Features
register(name, schema, description?)/get(name)/has(name)/getDescription(name).getNames(),getAll(),remove(name),clear().extend(baseName, name, extension),partial(baseName, name),pick(baseName, name, keys[]),omit(baseName, name, keys[]).createSchemaRegistry()factory — no global accessor.defineSchema(schema, meta, registry)— registry argument is required.- Typed
OPENAPI.SCHEMA_NOT_FOUND/OPENAPI.INVALID_BASE_SCHEMAerrors from derivation helpers.
Usage: Basic
import z from "zod";
import {
createSchemaRegistry,
defineSchema,
} from "@suaveplan/api-openapi/schema-registry";
const registry = createSchemaRegistry();
defineSchema(z.object({ id: z.string(), email: z.string() }), { name: "User" }, registry);
const User = registry.get("User");
Usage: Advanced
import z from "zod";
import {
createSchemaRegistry,
} from "@suaveplan/api-openapi/schema-registry";
const registry = createSchemaRegistry();
registry.register(
"User",
z.object({
id: z.string().uuid(),
email: z.string().email(),
password: z.string(),
}),
"Internal user shape including hashed password"
);
// Public shape — drop `password`.
const UserPublic = registry.omit("User", "UserPublic", ["password"]);
// Patch shape — every field optional.
const UserPatch = registry.partial("User", "UserPatch");
// Extended shape — add audit field.
const UserAudit = registry.extend("User", "UserAudit", {
lastSeenAt: z.string().datetime(),
});
console.log(registry.getNames()); // ["User","UserPublic","UserPatch","UserAudit"]
API Reference
class SchemaRegistryregister<T>(name, schema, description?): Tget<T>(name): T | undefinedhas(name): booleangetDescription(name): string | undefinedgetNames(): string[]getAll(): Map<string, z.ZodType>remove(name): booleanclear(): voidextend(baseName, name, extension): z.ZodObject— throwsOPENAPI.SCHEMA_NOT_FOUNDorOPENAPI.INVALID_BASE_SCHEMApartial(baseName, name): z.ZodObject— same throw semanticspick(baseName, name, keys[]): z.ZodObject— same throw semanticsomit(baseName, name, keys[]): z.ZodObject— same throw semantics
function createSchemaRegistry(): SchemaRegistryfunction defineSchema<T>(schema: T, meta: {name, description?}, registry: SchemaRegistry): T
Implementation Notes
requireObjectBase is the central guard. It throws
OPENAPI.SCHEMA_NOT_FOUND when the name is unknown and
OPENAPI.INVALID_BASE_SCHEMA when the registered schema is not a
ZodObject. Both branches carry the base name in the error context so
the call site can be diagnosed without a stack walk.
pick and omit take a string[] keys list rather than the Zod
shape-mask object because OpenAPI consumers usually serialise key lists
from configuration, not from compile-time literals. The shape mask is
constructed internally to match Zod's expected input.
defineSchema requires the registry argument by position (third arg).
Calling it without is a compile error. This is the deliberate behavioural
divergence from upstream's defineSchema(schema, meta) which always
wrote to a process-global registry — a footgun that turned every test
file into a coupled neighbour.
The class uses private readonly Maps for schemas and descriptions.
External mutation is impossible without going through register /
remove / clear, which gives the registry well-defined ownership of
both maps and lets getAll() safely return a fresh copy.