@valibot/to-json-schema
Version:
The official JSON schema converter for Valibot
481 lines (476 loc) • 12.6 kB
JavaScript
// src/convertSchema.ts
import * as v from "valibot";
// src/utils/handleError.ts
function handleError(message, config) {
switch (config?.errorMode) {
case "ignore": {
break;
}
case "warn": {
console.warn(message);
break;
}
default: {
throw new Error(message);
}
}
}
// src/convertAction.ts
function convertAction(jsonSchema, valibotAction, config) {
switch (valibotAction.type) {
case "base64": {
jsonSchema.contentEncoding = "base64";
break;
}
case "bic":
case "cuid2":
case "decimal":
case "digits":
case "emoji":
case "hexadecimal":
case "hex_color":
case "nanoid":
case "octal":
case "ulid": {
jsonSchema.pattern = valibotAction.requirement.source;
break;
}
case "description": {
jsonSchema.description = valibotAction.description;
break;
}
case "email": {
jsonSchema.format = "email";
break;
}
case "empty": {
if (jsonSchema.type === "array") {
jsonSchema.maxItems = 0;
} else {
if (jsonSchema.type !== "string") {
handleError(
`The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`,
config
);
}
jsonSchema.maxLength = 0;
}
break;
}
case "integer": {
jsonSchema.type = "integer";
break;
}
case "ipv4": {
jsonSchema.format = "ipv4";
break;
}
case "ipv6": {
jsonSchema.format = "ipv6";
break;
}
case "iso_date": {
jsonSchema.format = "date";
break;
}
case "iso_date_time":
case "iso_timestamp": {
jsonSchema.format = "date-time";
break;
}
case "iso_time": {
jsonSchema.format = "time";
break;
}
case "length": {
if (jsonSchema.type === "array") {
jsonSchema.minItems = valibotAction.requirement;
jsonSchema.maxItems = valibotAction.requirement;
} else {
if (jsonSchema.type !== "string") {
handleError(
`The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`,
config
);
}
jsonSchema.minLength = valibotAction.requirement;
jsonSchema.maxLength = valibotAction.requirement;
}
break;
}
case "max_length": {
if (jsonSchema.type === "array") {
jsonSchema.maxItems = valibotAction.requirement;
} else {
if (jsonSchema.type !== "string") {
handleError(
`The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`,
config
);
}
jsonSchema.maxLength = valibotAction.requirement;
}
break;
}
case "max_value": {
if (jsonSchema.type !== "number") {
handleError(
`The "max_value" action is not supported on type "${jsonSchema.type}".`,
config
);
}
jsonSchema.maximum = valibotAction.requirement;
break;
}
case "min_length": {
if (jsonSchema.type === "array") {
jsonSchema.minItems = valibotAction.requirement;
} else {
if (jsonSchema.type !== "string") {
handleError(
`The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`,
config
);
}
jsonSchema.minLength = valibotAction.requirement;
}
break;
}
case "min_value": {
if (jsonSchema.type !== "number") {
handleError(
`The "min_value" action is not supported on type "${jsonSchema.type}".`,
config
);
}
jsonSchema.minimum = valibotAction.requirement;
break;
}
case "multiple_of": {
jsonSchema.multipleOf = valibotAction.requirement;
break;
}
case "non_empty": {
if (jsonSchema.type === "array") {
jsonSchema.minItems = 1;
} else {
if (jsonSchema.type !== "string") {
handleError(
`The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`,
config
);
}
jsonSchema.minLength = 1;
}
break;
}
case "regex": {
if (valibotAction.requirement.flags) {
handleError("RegExp flags are not supported by JSON Schema.", config);
}
jsonSchema.pattern = valibotAction.requirement.source;
break;
}
case "title": {
jsonSchema.title = valibotAction.title;
break;
}
case "url": {
jsonSchema.format = "uri";
break;
}
case "uuid": {
jsonSchema.format = "uuid";
break;
}
case "value": {
jsonSchema.const = valibotAction.requirement;
break;
}
default: {
handleError(
// @ts-expect-error
`The "${valibotAction.type}" action cannot be converted to JSON Schema.`,
config
);
}
}
return jsonSchema;
}
// src/convertSchema.ts
var refCount = 0;
function convertSchema(jsonSchema, valibotSchema, config, context) {
const referenceId = context.referenceMap.get(valibotSchema);
if (referenceId && referenceId in context.definitions) {
jsonSchema.$ref = `#/$defs/${referenceId}`;
return jsonSchema;
}
if ("pipe" in valibotSchema) {
for (let index = 0; index < valibotSchema.pipe.length; index++) {
const valibotPipeItem = valibotSchema.pipe[index];
if (valibotPipeItem.kind === "schema") {
if (index > 0) {
handleError(
'A "pipe" with multiple schemas cannot be converted to JSON Schema.',
config
);
}
const tempJsonSchema = convertSchema(
{},
valibotPipeItem,
config,
context
);
if (tempJsonSchema.$ref) {
const referenceId2 = tempJsonSchema.$ref.split("/")[2];
Object.assign(jsonSchema, context.definitions[referenceId2]);
} else {
Object.assign(jsonSchema, tempJsonSchema);
}
} else {
jsonSchema = convertAction(jsonSchema, valibotPipeItem, config);
}
}
return jsonSchema;
}
switch (valibotSchema.type) {
// Primitive schemas
case "boolean": {
jsonSchema.type = "boolean";
break;
}
case "null": {
jsonSchema.type = "null";
break;
}
case "number": {
jsonSchema.type = "number";
break;
}
case "string": {
jsonSchema.type = "string";
break;
}
// Complex schemas
case "array": {
jsonSchema.type = "array";
jsonSchema.items = convertSchema(
{},
valibotSchema.item,
config,
context
);
break;
}
case "tuple":
case "tuple_with_rest":
case "loose_tuple":
case "strict_tuple": {
jsonSchema.type = "array";
jsonSchema.items = [];
for (const item of valibotSchema.items) {
jsonSchema.items.push(
convertSchema({}, item, config, context)
);
}
if (valibotSchema.type === "tuple_with_rest") {
jsonSchema.additionalItems = convertSchema(
{},
valibotSchema.rest,
config,
context
);
} else {
jsonSchema.additionalItems = valibotSchema.type === "loose_tuple";
}
break;
}
case "object":
case "object_with_rest":
case "loose_object":
case "strict_object": {
jsonSchema.type = "object";
jsonSchema.properties = {};
jsonSchema.required = [];
for (const key in valibotSchema.entries) {
const entry = valibotSchema.entries[key];
jsonSchema.properties[key] = convertSchema({}, entry, config, context);
if (entry.type !== "nullish" && entry.type !== "optional") {
jsonSchema.required.push(key);
}
}
if (valibotSchema.type === "object_with_rest") {
jsonSchema.additionalProperties = convertSchema(
{},
valibotSchema.rest,
config,
context
);
} else if (valibotSchema.type === "strict_object") {
jsonSchema.additionalProperties = false;
}
break;
}
case "record": {
if ("pipe" in valibotSchema.key) {
handleError(
'The "record" schema with a schema for the key that contains a "pipe" cannot be converted to JSON Schema.',
config
);
}
if (valibotSchema.key.type !== "string") {
handleError(
`The "record" schema with the "${valibotSchema.key.type}" schema for the key cannot be converted to JSON Schema.`,
config
);
}
jsonSchema.type = "object";
jsonSchema.additionalProperties = convertSchema(
{},
valibotSchema.value,
config,
context
);
break;
}
// Special schemas
case "any":
case "unknown": {
break;
}
case "nullable":
case "nullish": {
jsonSchema.anyOf = [
convertSchema(
{},
valibotSchema.wrapped,
config,
context
),
{ type: "null" }
];
if (valibotSchema.default !== void 0) {
jsonSchema.default = v.getDefault(valibotSchema);
}
break;
}
case "exact_optional":
case "optional":
case "undefinedable": {
jsonSchema = convertSchema(
jsonSchema,
valibotSchema.wrapped,
config,
context
);
if (valibotSchema.default !== void 0) {
jsonSchema.default = v.getDefault(valibotSchema);
}
break;
}
case "literal": {
if (typeof valibotSchema.literal !== "boolean" && typeof valibotSchema.literal !== "number" && typeof valibotSchema.literal !== "string") {
handleError(
'The value of the "literal" schema is not JSON compatible.',
config
);
}
jsonSchema.const = valibotSchema.literal;
break;
}
case "enum": {
jsonSchema.enum = valibotSchema.options;
break;
}
case "picklist": {
if (valibotSchema.options.some(
(option) => typeof option !== "number" && typeof option !== "string"
)) {
handleError(
'An option of the "picklist" schema is not JSON compatible.',
config
);
}
jsonSchema.enum = valibotSchema.options;
break;
}
case "union":
case "variant": {
jsonSchema.anyOf = valibotSchema.options.map(
(option) => convertSchema({}, option, config, context)
);
break;
}
case "intersect": {
jsonSchema.allOf = valibotSchema.options.map(
(option) => convertSchema({}, option, config, context)
);
break;
}
case "lazy": {
let wrappedValibotSchema = context.getterMap.get(valibotSchema.getter);
if (!wrappedValibotSchema) {
wrappedValibotSchema = valibotSchema.getter(void 0);
context.getterMap.set(valibotSchema.getter, wrappedValibotSchema);
}
let referenceId2 = context.referenceMap.get(wrappedValibotSchema);
if (!referenceId2) {
referenceId2 = `${refCount++}`;
context.referenceMap.set(wrappedValibotSchema, referenceId2);
context.definitions[referenceId2] = convertSchema(
{},
wrappedValibotSchema,
config,
context
);
}
jsonSchema.$ref = `#/$defs/${referenceId2}`;
break;
}
// Other schemas
default: {
handleError(
// @ts-expect-error
`The "${valibotSchema.type}" schema cannot be converted to JSON Schema.`,
config
);
}
}
return jsonSchema;
}
// src/toJsonSchema.ts
function toJsonSchema(schema, config) {
const context = {
definitions: {},
referenceMap: /* @__PURE__ */ new Map(),
getterMap: /* @__PURE__ */ new Map()
};
if (config?.definitions) {
for (const key in config.definitions) {
context.referenceMap.set(config.definitions[key], key);
}
for (const key in config.definitions) {
context.definitions[key] = convertSchema(
{},
// @ts-expect-error
config.definitions[key],
config,
context
);
}
}
const jsonSchema = convertSchema(
{ $schema: "http://json-schema.org/draft-07/schema#" },
// @ts-expect-error
schema,
config,
context
);
if (context.referenceMap.size) {
jsonSchema.$defs = context.definitions;
}
return jsonSchema;
}
export {
toJsonSchema
};