UNPKG

@valibot/to-json-schema

Version:

The official JSON schema converter for Valibot

481 lines (476 loc) 12.6 kB
// 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 };