JSON Schema package upgrade

Valibot's JSON Schema package has seen significant growth in adoption over the past few weeks, reaching almost 200,000 monthly downloads on npm. We believe it is particularly popular for documentation and code generation purposes via the OpenAPI specification, as well as for generating structured LLM outputs.
As these use cases will probably become more common in future, we have listened to your feedback and decided to invest more time in developing the package to make it extremely powerful. This blog post will introduce the new features added in the last two minor versions.
Convert input or output of schema
The JSON Schema package now supports a new typeMode
configuration option, which allows you to specify whether you want to convert the input or output of a Valibot schema.
This is particularly useful when validating and defining an API endpoint with Valibot and your schemas contain transformations. This is because external developers are usually interested in the input schema of the request data but in the output schema of the response data.
import * as v from 'valibot';
import { toJsonSchema } from '@valibot/to-json-schema';
const ValibotSchema = v.pipe(
v.string(),
v.decimal(),
v.transform(Number),
v.number(),
v.maxValue(100)
);
toJsonSchema(ValibotSchema, { typeMode: 'input' });
// {
// $schema: "http://json-schema.org/draft-07/schema#",
// type: "string",
// pattern: "^[+-]?(?:\\d*\\.)?\\d+$"
// }
toJsonSchema(ValibotSchema, { typeMode: 'output' });
// {
// $schema: "http://json-schema.org/draft-07/schema#",
// type: "number",
// maximum: 100
// }
Override default JSON Schema conversion
The JSON Schema package now enables you to override the default behaviour of the JSON Schema conversion process. This can be achieved using the three new configuration options: overrideSchema
, overrideAction
and overrideRef
. These let you specify a custom function that will be called for each schema, action or reference during conversion.
You can either return a value to override the default behaviour, or return null
or undefined
to skip the override. Furthermore, all three callback functions provide the full context via the first function argument, enabling you to perform the same actions as we do internally.
import * as v from 'valibot';
import { toJsonSchema } from '@valibot/to-json-schema';
const ValibotSchema = v.object({ createdAt: v.date() });
toJsonSchema(ValibotSchema, {
overrideSchema(context) {
if (context.valibotSchema.type === 'date') {
return { type: 'string', format: 'date-time' };
}
},
});
// {
// $schema: "http://json-schema.org/draft-07/schema#",
// type: "object",
// properties: {
// createdAt: { type: "string" format: "date-time" }
// },
// required: ["createdAt"]
// }
New global definition storage
If you are reusing Valibot schemas within other Valibot schemas, you may be interested in representing these schemas as references in the JSON Schema output. To facilitate this, the JSON Schema package now includes a new global definition storage.
This feature allows you to define these definitions after creating a Valibot schema, rather than when calling toJsonSchema
. This can be particularly useful for larger projects with many schemas, as it helps to keep your code clean and organised.
import * as v from 'valibot';
import { addGlobalDefs, toJsonSchema } from '@valibot/to-json-schema';
const ValibotSchema1 = v.string();
const ValibotSchema2 = v.number();
addGlobalDefs({ ValibotSchema1, ValibotSchema2 });
const ValibotSchema3 = v.tuple([ValibotSchema1, ValibotSchema2]);
toJsonSchema(ValibotSchema3);
// {
// $schema: "http://json-schema.org/draft-07/schema#",
// type: "array",
// items: [
// { $ref: "#/$defs/ValibotSchema1" },
// { $ref: "#/$defs/ValibotSchema2" }
// ],
// minItems: 2,
// $defs: {
// ValibotSchema1: { type: "string" },
// ValibotSchema2: { type: "number" }
// }
// }
Output schema definitions only
If you're working with the OpenAPI specification, you might be interested in generating only the JSON Schema definitions and overriding the reference IDs to customise them. The new toJsonSchemaDefs
function and overrideRef
configuration option now make this possible.
import * as v from 'valibot';
import { toJsonSchemaDefs } from '@valibot/to-json-schema';
const ValibotSchema1 = v.string();
const ValibotSchema2 = v.number();
const ValibotSchema3 = v.tuple([ValibotSchema1, ValibotSchema2]);
toJsonSchemaDefs(
{ ValibotSchema1, ValibotSchema2, ValibotSchema3 },
{ overrideRef: (context) => `#/schemas/${context.referenceId}` }
);
// {
// ValibotSchema1: { type: "string" },
// ValibotSchema2: { type: "number" },
// ValibotSchema3: {
// type: "array",
// items: [
// { $ref: "#/schemas/ValibotSchema1" },
// { $ref: "#/schemas/ValibotSchema2" }
// ],
// minItems: 2
// }
// }
You can also convert global definitions added via the new addGlobalDefs
function. To do this, call getGlobalDefs
to retrieve the definitions and pass them as the first argument to toJsonSchemaDefs
.
Enhanced metadata support
Previously we only supported the title
and description
action directly but not the generic metadata
action. This was improved so that title, description and examples can now also be specified via the generic metadata
action, providing you with more flexibility in defining your schemas.
import * as v from 'valibot';
import { toJsonSchema } from '@valibot/to-json-schema';
const ValibotSchema = v.pipe(
v.string(),
v.email(),
v.metadata({
title: 'Email Schema',
description: 'A schema that validates email addresses.',
examples: ['jane@example.com'],
})
);
toJsonSchema(ValibotSchema);
// {
// $schema: "http://json-schema.org/draft-07/schema#",
// type: "string",
// format: "email",
// title: "Email Schema",
// description: "A schema that validates email addresses.",
// examples: ["jane@example.com"]
// }
What's next?
Firstly, I would like to thank @Xiot for his detailed feedback on the JSON Schema package and all the new changes. His help in identifying edge cases and advising on the API design was invaluable.
Next, we will probably release the Zod-to-Valibot codemod to help you migrate to Valibot if you are interested. Stay tuned for that!