Skip to main content

Discriminators

When defining a Union Schema, a unionDiscriminator handler is used to decide which alternative union schema to use.

Discriminators may need to inspect values in the output target value, which means they need to either use prebuilt keywords like $reference to get these values, or they often need to be implemented as a function
using the full value processor signature.

If the discriminator returns undefined, it means the union schema cannot yet be resolved, and discrimination will be retried on another traversal pass.

Upon success, the discriminator should return a reference to the resolved schema, either the key of the unionSchema, or the actual CompiledSchema value. (Note: not the pre-compilation Schema!)

import { Schema, SchemaResolver } from '@versionzero/schema';
const resolver = new SchemaResolver();

export const schema = resolver.compile(
new Schema()
.unionDiscriminator(data => {
const contentType = data?.['content-type'];
if (!contentType) {
return undefined;
}
if (/.+\/.*json/.test(contentType)) {
return 'json';
}
else if (contentType.startsWith('text/')) {
return 'text'
}
else {
return 'data';
}
})
/* equivalent in declarative form; less obvious, but allows schema serialization
.unionDiscriminator([
{$get: 'content-type'},
{$first: [
{$when: [{$matches: /.+\/.*json/}, 'json']},
{$when: [{$matches: /text\/.+/}, 'text']},
{$when: ['$defined', 'data']},
]}])
*/
.unionSchema('text', new Schema('object')
.property('content-type', new Schema('string')
.default('text/plain')
)
.property('content', new Schema('string'))
)
.unionSchema('json', new Schema('object')
.property('content-type', new Schema('string')
.required()
.normalizer({$literal: 'application/json'})
)
.property('content', new Schema('string').validator('$json'))
)
.unionSchema('data', new Schema('object')

.property('content-type', new Schema('string').required())
.property('content', new Schema('string').validator('$base64'))
)
);

const content1 = {'content-type': 'text/plain', content: 'hello' };
console.log( await schema.process( content1 ) );
const content2 = {'content-type': 'application/foo+json', content: '{"stuff":123}'}
console.log( await schema.process( content2 ))
const content3 = {'content-type': 'application/octet-stream', 'content': 'SGVsbG8sIFdvcmxkIQ=='}
console.log( await schema.process( content3))

Automatic Discrimination

It isn't always necessary to provide a unionDiscriminator handler if the compiler determines that one can be automatically synthesized:

  • If the union schemas all contain unique properties.
  • If the union schemas contain common properties that are constrained to unique values.

In these cases, the discriminator will be created during compilation, and the referenced properties will be "hoisted" to the containing schema in order to make them available for assignment before resolution.

See the Union guide and the examples in the project repo for more details.