Getting Started
A schema is a structured definition of data.
- The schema can be treated as a contract to validate input data.
- A schema can also be used as a blueprint to process input data into valid output.
As is tradition... hello!
Here's a trivial string schema:
import { Schema, SchemaResolver } from '@versionzero/schema';
// The schema resolver provides compilation services and an
// extensible registry of named schemas and value processors.
const resolver = new SchemaResolver();
// Here we'll define our `Schema`:
export const helloSchemaSource = new Schema('string')
.normalizer('$title-case')
.validator({$matches: /^Hello.+/})
const helloSchema = resolver.compile(helloSchemaSource);
// Processing passes the input through the normalizer then the validator;
// validation only checks (it doesn't normalize), so the case matters.
const casual = 'hello world';
console.log( await helloSchema.process(casual) );
// → "Hello World"
await helloSchema.validate(casual).catch((err) => console.error(err.message));
// → ValidationError (does not match pattern; wrong case)
const proper = 'Hello Friend';
console.log( await helloSchema.validate(proper) );
// → "Hello Friend"
const notAString = 123;
await helloSchema.validate(notAString).catch((err) => console.error(err.message));
// → ValidationError (does not match pattern)
More complex...
This schema defines the data structure of a meeting.
import { Schema, SchemaResolver } from '@versionzero/schema';
export const resolver = new SchemaResolver();
// you can define a schema to reference inline in multiple places...
const meetingTextFieldSchema = new Schema('string')
.normalizer('$trim')
.validator({$length: {min: 1, max: 1024}});
// or you can register it to the resolver to reference by name
resolver.registerSchema('meeting-text', meetingTextFieldSchema);
export const schema = new Schema('object')
.property('id', new Schema('string')
.required()
.default(() => crypto.randomUUID())
.normalizer(['$trim', '$lowercase'])
.validator('$uuid')
)
.property('title', new Schema(meetingTextFieldSchema) // extend a schema instance
.required()
.default('Untitled Meeting')
)
.property('description', new Schema('meeting-text')) // or extend a named schema
.property('starts', new Schema('date').required())
.property('ends', new Schema('date')
.required()
.validator({'$date-range': {min: {$reference: '^starts'}}})
)
.property('attendees', new Schema('array')
.required()
.validator({$length: {min: 1}})
.property('*', new Schema('object')
.property('email', new Schema('string')
.required()
.validator('$email')
)
.property('response', new Schema('string')
.default('pending')
.validator({$in: ['accepted', 'declined', 'tentative', 'pending']})
)
)
)
const meetingSchema = resolver.compile(schema);
// Here's a valid meeting.
const meeting = {
id: '123e4567-e89b-12d3-a456-426614174000',
title: 'Weekly Team Meeting',
description: 'Weekly team meeting to discuss progress and upcoming projects.',
starts: new Date('2026-09-01T10:00:00'),
ends: new Date('2026-09-01T11:00:00'),
attendees: [
{ email: 'john.doe@example.com', response: 'accepted' },
{ email: 'jane.smith@example.com', response: 'declined' },
{ email: 'ted.richards@example.com' },
{ email: 'alice.johnson@example.com', response: 'tentative' }
]
}
// The meeting schema is usable both as a contract for
// validation:
console.log( await meetingSchema.validate(meeting) );
// → (returns its input)
// ...as well as a blueprint for processing:
const minimal = {
starts: '2027-01-01T10:00:00',
ends: '2027-01-01T11:00:00',
attendees: [ { email: 'john.doe@example.com' } ]
}
// When processed, it populates defaults:
console.log( await meetingSchema.process(minimal) );
// → {
// id: '1f721f55-c075-485c-994c-96dbb30b035d',
// title: 'Untitled Meeting',
// starts: 2027-01-01T18:00:00.000Z,
// ends: 2027-01-01T19:00:00.000Z,
// attendees: [ { email: 'john.doe@example.com', response: 'pending' } ]
// }