Skip to main content

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();

// We define our `Schema`, and compile it:
const helloSchema = await resolver.compile(
new Schema('string')
.normalizer('$title-case')
.validator({$matches: /^Hello.+/})
);

// Processing will pass the input through both the normalizer
// and validator pipelines:
const greeting = await helloSchema.process('hello world');
console.log(greeting); // normalized to title case; prints Hello World

// Validation only runs the validator pipeline:
await helloSchema.validate('Hello Friend'); // succeeds..
await helloSchema.validate(123); // throws a ValidationError (not a string)
await helloSchema.validate('hello world'); // throws a ValidationError (incorrect capitalization)

More complex...

This schema defines the data structure of a meeting.

import crypto from 'node:crypto';
import { Schema, SchemaResolver } from '@versionzero/schema';

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);

const meetingSchema = await resolver.compile(
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']})
)
)
)
)

Notice above how the ends validator references starts to enforce an internal value relationship.

The meeting schema is usable both as a contract for validation:

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' }
]
}

const validatedMeeting = await meetingSchema.validate(meeting); // valid!

as well as defining a blueprint for processing:

const minimal = {
starts: '2027-01-01T10:00:00',
ends: '2027-01-01T11:00:00',
attendees: [ { email: 'john.doe@example.com' } ]
}

// would not validate as-is, but...
const processedMeeting = await meetingSchema.process(minimal);

/* Result is now valid - populated with defaults, date strings converted to Dates:
{
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' } ]
}
*/