Unions
Unions allow you to create schemas that have multiple alternative definitions.
If every union member has unique properties, they can be automatically discriminated.
import { Schema, SchemaResolver } from '@versionzero/schema';
const resolver = new SchemaResolver();
const notificationSchema = await resolver.compile(
new Schema('object')
.unionSchema('sms', new Schema('object')
.property('phone', new Schema('string').validator('$phone'))
)
.unionSchema('email', new Schema('object')
.property('address', new Schema('string').validator('$email'))
)
.unionSchema('webhook', new Schema('object')
.property('url', new Schema('string').validator('$url'))
.property('method', new Schema('string').values(['GET', 'POST']).default('GET'))
)
)
console.log( await notificationSchema.process( { address: 'user@example.tld' } ));
// -> { address: 'user@example.tld' }
console.log( await notificationSchema.process( { url: 'https://localhost:3000' } ));
// -> { url: 'https://localhost:3000/', method: 'GET' }
If the union members have a property in common, but each is constrained to unique values (using Schema.literal
or the values option), they can also be automatically discriminated:
import { Schema, SchemaResolver } from '@versionzero/schema';
const resolver = new SchemaResolver();
const paymentSchema = await resolver.compile(
new Schema()
.unionSchema('card', new Schema('object')
.property('type', Schema.literal('card').required())
.property('cardNumber', new Schema('string').validator('$cardnum').required())
.property('cvv', new Schema('string').required().validator({$length: {min: 3}}))
.property('email', new Schema('string').validator('$email')) // optional
)
.unionSchema('paypal', new Schema('object')
.property('type', Schema.literal('paypal').required())
.property('email', new Schema('string').required().validator('$email'))
)
);
console.log( await paymentSchema.process({ type: 'card', cardNumber: '4111 1111 1111 1111', cvv: '123' }) );
console.log( await paymentSchema.process({ type: 'paypal', email: 'user@example.com' }) );
The properties used for automatic discrimination are "hoisted" to the containing union.
If your schema has more complex discrimination requirements, you can provide a unionDiscriminator handler.
They must return either the unionSchema string key or a compiled unionSchema reference. Here we take
advantage of the $type processor to select the appropriate schema based on type of the input value:
import { Schema, SchemaResolver } from '@versionzero/schema';
const resolver = new SchemaResolver();
const colorComponent = new Schema()
.default(0)
.required()
.normalizer(['$number', '$integer', {$range: {min: 0, max: 255}}])
.transformer([v => v.toString(16), {$pad: {width: 2, char: '0'}}])
const colorSchema = await resolver.compile(
new Schema()
.unionDiscriminator('$type')
.unionSchema('object', new Schema()
.opaque()
.property('red', new Schema(colorComponent))
.property('green', new Schema(colorComponent))
.property('blue', new Schema(colorComponent))
.transformer(c => `#${c.red}${c.green}${c.blue}`)
)
.unionSchema('string', new Schema('string')
.validator(['$lowercase', {$matches: /#[0-9a-f]{6}/}])
)
)
console.log( await colorSchema.process( {red: 255, green: 255, blue: 0}));
// -> #ffff00
console.log( await colorSchema.process('#00ff00') );
// -> #00ff00
See the discriminator handler documentation guide and the examples in the project repo for more details.