Small, fast, eval-less, Cloudflare Workers compatible, dynamic JSON Schema validator.
Cabidela is a small, fast, eval-less, Cloudflare Workers compatible, dynamic JSON Schema validator. It implements a large subset of https://json-schema.org/draft/2020-12/json-schema-validation that should cover most use-cases. But not all. See limitations below.
Install the package:
npm install @cloudflare/cabidela --save
Import it:
import { Cabidela } from "@cloudflare/cabidela";
Use it:
let schema: any = {
type: "object",
properties: {
prompt: {
type: "string",
minLength: 1,
maxLength: 131072,
description: "The input text prompt for the model to generate a response.",
},
num_steps: {
type: "number",
minimum: 0,
maximum: 20,
description: "Increases the likelihood of the model introducing new topics.",
},
},
required: ["prompt"],
};
const cabidela = new Cabidela(schema);
cabidela.validate({
prompt: "Tell me a joke",
num_steps: 5,
});
Cabidela implements a Exception-Driven Validation approach. If any condition in the schema is not met, we throw an error.
const cabidela = new Cabidela(schema: any, options?: CabidelaOptions)
Cabidela takes a JSON-Schema and optional configuration flags:
applyDefaults
: boolean - If true, the validator will apply default values to the input object. Default is false.errorMessages
: boolean - If true, the validator will use customerrorMessage
messages from the schema. Default is false.fullErrors
: boolean - If true, the validator will be more verbose when throwing errors for complex schemas (example: anyOf, oneOf's), set to false for shorter exceptions. Default is true.useMerge
: boolean - Set to true if you want to use the$merge
keyword. Default is false. See below for more information.subSchemas
: any[] - An optional array of sub-schemas that can be used with$id
and$ref
. See below for more information.
Returns a validation object.
You can change the schema at any time by calling cabidela.setSchema(schema: any)
.
You can change the options at any time by calling cabidela.setOptions(options: CabidelaOptions)
.
Call cabidela.validate(payload: any)
to validate your payload.
Returns truth if the payload is valid, throws an error otherwise.
const payload = {
messages: [
{ role: "system", content: "You're a helpful assistant" },
{ role: "user", content: "What is Cloudflare?" },
],
};
try {
cabidela.validate(payload);
console.log("Payload is valid");
} catch (e) {
console.error(e);
}
Some options, like applyDefaults
, will modify the input object.
const schema = {
type: "object",
properties: {
prompt: {
type: "string",
},
num_steps: {
type: "number",
default: 10,
},
},
};
const cabidela = new Cabidela(schema, { applyDefaults: true });
const payload = {
prompt: "Tell me a joke",
});
cabidela.validate(payload);
console.log(payload);
// {
// prompt: 'Tell me a joke',
// num_steps: 10
// }
Using applyDefaults
with oneOf
will one apply the default value of the sub-schema that matches the condition. For
example, using this schema:
{
type: "object",
oneOf: [
{
type: "object",
properties: {
sun: {
type: "number",
default: 9000,
},
moon: {
type: "number",
default: 9000,
},
},
required: ["sun"],
},
{
type: "object",
properties: {
sun: {
type: "number",
default: 9000,
},
moon: {
type: "number",
default: 9000,
},
},
required: ["moon"],
},
],
};
- The payload
{ sun: 10}
will be modified to{ sun: 10, moon: 9000 }
. - The payload
{ moon: 10}
will be modified to{ sun: 9000, moon: 10 }
. - The payload
{ saturn: 10}
will throw an error because no condition is met.
The keywords $id, $ref and $defs can be used to build and maintain complex schemas where the reusable parts are defined in separate schemas.
The following is the main schema and a customer
sub-schema that defines the contacts
and address
properties.
import { Cabidela } from "@cloudflare/cabidela";
const schema = {
$id: "http://example.com/schemas/main",
type: "object",
properties: {
name: { type: "string" },
contacts: { $ref: "customer#/contacts" },
address: { $ref: "customer#/address" },
balance: { $ref: "$defs#/balance" },
},
required: ["name", "contacts", "address"],
"$defs": {
"balance": {
type: "object",
prope properties: {
currency: { type: "string" },
amount: { type: "number" },
},
}
}
};
const contactSchema = {
$id: "http://example.com/schemas/customer",
contacts: {
type: "object",
properties: {
email: { type: "string" },
phone: { type: "string" },
},
required: ["email", "phone"],
},
address: {
type: "object",
properties: {
street: { type: "string" },
city: { type: "string" },
zip: { type: "string" },
country: { type: "string" },
},
required: ["street", "city", "zip", "country"],
},
};
const cabidela = new Cabidela(schema, { subSchemas: [contactSchema] });
cabidela.validate({
name: "John",
contacts: {
email: "[email protected]",
phone: "+123456789",
},
address: {
street: "123 Main St",
city: "San Francisco",
zip: "94105",
country: "USA",
},
});
The standard way of combining and extending schemas is by using the allOf
(AND), anyOf
(OR), oneOf
(XOR) and not
keywords, all supported by this library.
Cabidela supports an additional keyword $merge
(inspired by Ajv) that allows you to merge two objects. This is useful when you want to extend a schema with additional properties and `allOf`` is not enough.
Here's how it works:
{
"$merge": {
"source": {
"type": "object",
"properties": { "p": { "type": "string" } },
"additionalProperties": false
},
"with": {
"properties": { "q": { "type": "number" } }
}
}
}
Resolves to:
{
"type": "object",
"properties": {
"p": {
"type": "string" }
},
"q": {
"type": "number"
}
},
"additionalProperties": false
}
To use $merge
set the useMerge
flag to true when creating the instance.
new Cabidela(schema, { useMerge: true });
You can combine $merge
with $id
and $ref
keywords, which get resolved first, for even more flexibility.
If the new instance options has the errorMessages
flag set to true, you can use the property errorMessage
in the schema to define custom error messages.
const schema = {
type: "object",
properties: {
prompt: {
type: "string",
},
},
required: ["prompt"],
errorMessage: "prompt required",
};
const cabidela = new Cabidela(schema, { errorMessages: true });
const payload = {
missing: "prompt",
});
cabidela.validate(payload);
// throws "Error: prompt required"
The tests can be found here.
Cabidela uses vitest to test internal methods and compliance with the JSON Schema specification. To run the tests type:
npm run test
You can also run the tests with Ajv, or both. This allows us to compare the results and double-check how we interpret the specification.
npm run test-ajv
npm run test-all
JSON Schema validators like Ajv tend to follow this pattern:
- Instantiate a validator.
- Compile the schema.
- Validate one or more payloads against the (compiled) schema.
All of these steps have a cost. Compiling the schema makes sense if you are going to validate multiple payloads in the same session. But in the case of a Workers application we typically want to validate with the HTTP request, one payload at a time, and then we discard the validator.
Cabidela skips the compilation step and validates the payload directly against the schema.
In our benchmarks, Cabidela is significantly faster than Ajv on all operations if you don't reuse the validator. Even when we skip the instantiation and compilation steps from Ajv, Cabidela still performs relatively well.
Here are some results:
Cabidela - benchmarks/00-basic.bench.js > allOf, two properties
1929.61x faster than Ajv
Cabidela - benchmarks/00-basic.bench.js > allOf, two objects
1351.41x faster than Ajv
Cabidela - benchmarks/00-basic.bench.js > anyOf, two conditions
227.48x faster than Ajv
Cabidela - benchmarks/00-basic.bench.js > oneOf, two conditions
224.49x faster than Ajv
Cabidela - benchmarks/80-big-ops.bench.js > Big array payload
386.44x faster than Ajv
Cabidela - benchmarks/80-big-ops.bench.js > Big object payload
6.08x faster than Ajv
Cabidela - benchmarks/80-big-ops.bench.js > Deep schema, deep payload
59.75x faster than Ajv
Cabidela - benchmarks/80-big-ops.bench.js > allOf, two properties
1701.95x faster than Ajv
Cabidela - benchmarks/80-big-ops.bench.js > allOf, two objects
1307.04x faster than Ajv
Cabidela - benchmarks/80-big-ops.bench.js > anyOf, two conditions
207.73x faster than Ajv
Cabidela - benchmarks/80-big-ops.bench.js > oneOf, two conditions
211.72x faster than Ajv
We use Vitest's bench feature to run the benchmarks. You can find the benchmarks in the benchmarks folder and you can run them with:
npm run benchmark
Cabidela supports most of JSON Schema specification, and should be useful for many applications, but it's not complete. Currently we do not support:
- Multiple (array of) types
{ "type": ["number", "string"] }
- Pattern properties
dependentRequired
,dependentSchemas
,If-Then-Else
yet.