Skip to Content
@jetio/schema-builder docs are live 🎉
GuideSchema Reuse & Extension

Schema Reuse & Extension

extend() — clone & modify

const baseUser = new SchemaBuilder() .object() .properties({ id: (s) => s.integer(), name: (s) => s.string(), email: (s) => s.string().format("email"), }) .required(["id", "name"]); const adminUser = new SchemaBuilder() .extend(baseUser) // clone the schema .properties({ role: (s) => s.string().const("admin"), permissions: (s) => s.array().items((s) => s.string()), }) .required(["role", "permissions"]) .build(); // baseUser is unchanged // adminUser has: id, name, email, role, permissions

Always extend a schema before building on it. You can replace most keywords by redefining them after extending.

$ref vs .extend() for types — $ref resolves at validation time but gives TypeScript { author?: unknown }. .extend() inlines the schema, so you get { author?: { id?: number } } — typed all the way down. See Type Inference → Schema Extension for the full type-level story.

Merge vs replace

Keywords that MERGE (new values are added to existing ones):

  • properties — new properties are added
  • patternProperties — new patterns are added
  • required — new required fields are added to the array
  • $defs / definitions — new definitions are added
  • dependentSchemas — new dependent schemas are added
  • dependencies — new dependencies are added

Keywords that REPLACE (override existing): all others — type, oneOf, anyOf, allOf, enum, const, items, etc.

const base = new SchemaBuilder() .object() .properties({ id: (s) => s.number(), name: (s) => s.string(), }) .required(["id", "name"]) .build(); // ADDS to properties and required (doesn't replace) const extended = new SchemaBuilder() .extend(base) .properties({ email: (s) => s.string() }) .required(["email"]) .build(); // properties = { id, name, email }, required = ["id", "name", "email"] // Remove a property const withoutName = new SchemaBuilder() .extend(base) .remove(["name"], ["properties"]) .build(); // properties = { id }, required = ["id"] // Make all optional const allOptional = new SchemaBuilder().extend(base).optional().build(); // properties = { id, name }, required = []

Modifying merged keywords

// Remove properties and update required new SchemaBuilder().extend(baseSchema).remove(["fieldName"], ["properties", "required"]).build(); // Make all fields optional new SchemaBuilder().extend(baseSchema).optional().build(); // Override a specific property by redefining it new SchemaBuilder() .extend(baseSchema) .properties({ existingField: (s) => s.string() }) // redefines the type .build();

Loading external schemas

When loading an external schema with file, url, or json, do it first — it replaces the entire schema object of the builder. The same applies to extend.

From URL

const schema = await new SchemaBuilder() .url("https://example.com/schemas/user.json") .extend() .properties({ extraField: (s) => s.string() }) .build();

From file

const schema = await new SchemaBuilder() .file("./schemas/base.json") .extend() .properties({ additionalProp: (s) => s.string() }) .build();

From JSON

const existingSchema = { type: "object", properties: { name: { type: "string" } }, }; const extended = new SchemaBuilder() .json(existingSchema) .extend() .properties({ age: (s) => s.number() }) .required(["name", "age"]) .build();

Composition with extend()

const timestampMixin = new SchemaBuilder().object().properties({ createdAt: (s) => s.string().format("date-time"), updatedAt: (s) => s.string().format("date-time"), }); const auditMixin = new SchemaBuilder().object().properties({ createdBy: (s) => s.string(), modifiedBy: (s) => s.string(), }); const fullSchema = new SchemaBuilder() .object() .properties({ id: (s) => s.integer(), data: (s) => s.string(), }) .allOf( (s) => s.extend(timestampMixin), (s) => s.extend(auditMixin), ) .build(); // Result has: id, data, createdAt, updatedAt, createdBy, modifiedBy
Last updated on