Files
skills/docs/api-stack/api/api-openapi/src/schema-registry/schema-registry.md
2026-05-29 08:20:56 +01:00

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_SCHEMA errors 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 SchemaRegistry
    • register<T>(name, schema, description?): T
    • get<T>(name): T | undefined
    • has(name): boolean
    • getDescription(name): string | undefined
    • getNames(): string[]
    • getAll(): Map<string, z.ZodType>
    • remove(name): boolean
    • clear(): void
    • extend(baseName, name, extension): z.ZodObject — throws OPENAPI.SCHEMA_NOT_FOUND or OPENAPI.INVALID_BASE_SCHEMA
    • partial(baseName, name): z.ZodObject — same throw semantics
    • pick(baseName, name, keys[]): z.ZodObject — same throw semantics
    • omit(baseName, name, keys[]): z.ZodObject — same throw semantics
  • function createSchemaRegistry(): SchemaRegistry
  • function 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.