Install
npm install desy
Basic usage
Creating a simple string schema
import {d} from 'desy';
const mySchema = d.string();
mySchema.validate('tuna');
mySchema.validate(12);
Key ideas
- be
simple
- be as
fast as point 1 allows
Key features
- Stop validating on the first error. Desy stops validating on the first error and returns it.
- A string is an indicator. Desy returns an empty string in a valid case. In case of an error, Desy returns a string with a description of the error.
- No throwing errors. Desy only returns an empty or non-empty string.
Examples
Deep schema
const schema = d.array(
d.object({
type: d.string().oneOf(['person']),
hair: d.string().oneOf(['blue', 'brown']),
active: d.boolean(),
name: d.string(),
age: d.number().int(),
hobbies: d.array(d.string()),
address: d.object({
street: d.string(),
zip: d.string(),
country: d.string(),
}),
}),
);
schema.validate(people);
Get schema's types
import {d, InferDesy} from 'desy';
const userSchema = d.object({
username: d.string(),
});
const error = userSchema.validate({username: 'Ludwig'});
type User = InferDesy<typeof user>;
API
Common
const schema = d.mixed();
schema.validate('a');
schema.validate('');
schema.validate(null);
.test(func: (value: sting) => string)
const schema = d.mixed().test((value) => {
if (value === 'world') {
return '';
}
return 'MUST BE WORLD';
});
schema.validate('hello');
schema.validate('world');
.description(text: string)
Describes the schema for a reader. Ignored by validate, used by
createJsonSchema.
const schema = d.string().description('Full name as in the passport');
schema.validate('John');
.message(message: string | (({path}) => string))
Replaces the error of any rule in the chain. A message written on a parent
replaces whatever came from below, including which field failed.
const schema = d.string().min(3).message('Name is too short');
schema.validate('a');
schema.validate(42);
const withPath = d.object({
name: d.string().message(({path}) => `${path} is wrong`),
});
withPath.validate({name: ''});
mixed
const schema = d.mixed();
schema.validate('a');
schema.validate('');
schema.validate(null);
string
const schema = d.string();
schema.validate('a');
schema.validate('');
schema.validate(null);
const schema = d.string().length(1);
schema.validate('aa');
schema.validate('a');
const schema = d.string().optional();
schema.validate('');
schema.validate('a');
.oneOf(variants: string[])
const schema = d.string().oneOf(['hello', 'world']);
schema.validate('hello');
schema.validate('world');
schema.validate('foo');
const schema = d.string().regexp(/hello/i);
schema.validate('123hello');
schema.validate('hell');
const schema = d.string().min(1);
schema.validate('');
schema.validate('a');
const schema = d.string().max(1);
schema.validate('aa');
schema.validate('a');
number
const schema = d.number();
schema.validate(42);
schema.validate('42');
.oneOf(variants: number[])
const schema = d.number().oneOf([1, 2, 3]);
schema.validate(2);
schema.validate(4);
const schema = d.number().int();
schema.validate(42);
schema.validate(42.2);
const schema = d.number().float();
schema.validate(42);
schema.validate(42.2);
const schema = d.number().min(1);
schema.validate(0);
schema.validate(1);
const schema = d.number().max(1);
schema.validate(1);
schema.validate(2);
boolean
const schema = d.boolean();
schema.validate(true);
schema.validate(false);
schema.validate(1);
const schema = d.boolean().true();
schema.validate(true);
schema.validate(false);
const schema = d.boolean().false();
schema.validate(true);
schema.validate(false);
date
const schema = d.date();
schema.validate(0);
schema.validate('2024-03-15T23:21:48.605Z');
schema.validate(new Date());
schema.validate(undefined);
const now = Date.now();
const schema = d.date().min(now);
schema.validate(now);
schema.validate(now - 1);
const now = Date.now();
const schema = d.date().max(now);
schema.validate(now);
schema.validate(now + 1);
null
const schema = d.null();
schema.validate(null);
schema.validate(undefined);
object
.object(objschema: Record<string, Schema>)
const schema = d.object({
name: d.sting(),
});
schema.validate({name: 'alex'});
schema.validate({name: 'alex', age: 42});
schema.validate({name: 42});
An object is strict by default: an unknown key is an error. Use .loose()
where a wider value is expected.
const schema = d
.object({
name: d.sting(),
})
.loose();
schema.validate({name: 'alex'});
schema.validate({name: 'alex', age: 42});
schema.validate({name: 42});
A field is optional when its own schema says so — .optional(),
.undefinable() or .nullable(), see schema. The object reads
optionality off the field, there is no separate list of optional keys.
const schema = d.object({
name: d.sting(),
nick: d.sting().optional(),
});
schema.validate({name: 'alex', nick: 'al'});
schema.validate({name: 'alex'});
schema.validate({nick: 'al'});
schema.validate({name: 'alex', nick: 42});
array
.array(schemas: Schema[])
const schema = d.arrar(d.sting());
schema.validate(['hello', 'world']);
schema.validate(['hello', 42]);
const schema = d.arrar(d.sting()).length(2);
schema.validate(['hello', 'world']);
schema.validate(['world']);
const schema = d.arrar(d.sting()).min(2);
schema.validate(['hello', 'world']);
schema.validate(['world']);
const schema = d.arrar(d.sting()).max(2);
schema.validate(['hello', 'world']);
schema.validate(['hello', 'world', 'foo']);
JSON Schema
createJsonSchema(schema) turns a desy schema into a JSON Schema
(draft 2020-12). Useful when the data you are about to validate comes from an
LLM: the same schema constrains the generation and checks the answer.
import {d, createJsonSchema} from 'desy';
const schema = d
.object({
sentiment: d.string().oneOf(['positive', 'negative']),
score: d.number().int().min(1).max(5).description('From 1 to 5'),
reply: d.string().undefinable(),
})
.description('Review breakdown');
createJsonSchema(schema);
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"properties": {
"sentiment": {
"type": "string",
"minLength": 1,
"enum": ["positive", "negative"]
},
"score": {
"type": "integer",
"minimum": 1,
"maximum": 5,
"description": "From 1 to 5"
},
"reply": {"type": "string", "minLength": 1}
},
"required": ["sentiment", "score"],
"description": "Review breakdown"
}
Worth knowing:
d.string() becomes minLength: 1 — an empty string is not valid in desy.
additionalProperties is always false, .loose() included. Strict
structured outputs require it, and a schema stricter than the validator never
rejects what desy accepts.
- A field is in
required unless its schema is undefinable() — JSON has no
undefined, absence is expressed by required alone.
nullable() becomes "type": ["string", "null"], and where the schema
enumerates values, null joins them: oneOf gives enum: ["a", "b", null],
a const turns into enum: [value, null]. String and number keywords stay
as they are — they do not apply to null.
oneOf becomes enum, for strings and numbers alike.
.message() never reaches the schema: an error message is for a person,
a description is for the model.
- Repeated constraints narrow instead of overwriting:
.min(2).min(5) gives
minLength: 5, two oneOf intersect. A second regexp on the same string
cannot be narrowed and throws.
Five rules do not convert and throw instead of being dropped: .test(),
number().float(), date().min(), date().max() and mixed().notVoid().
Describe such a limit with .description() so the model still learns about it.
benchmark
smaller is better