import * as v from "valibot";
//#region src/utils/addError.ts
/**
* Adds an error message to the errors array.
*
* @param errors The array of error messages.
* @param message The error message to add.
*
* @returns The new errors.
*/
function addError(errors, message) {
if (errors) {
errors.push(message);
return errors;
}
return [message];
}
//#endregion
//#region src/utils/handleError.ts
/**
* Throws an error or logs a warning based on the configuration.
*
* @param message The message to throw or log.
* @param config The conversion configuration.
*/
function handleError(message, config) {
switch (config?.errorMode) {
case "ignore": break;
case "warn": {
console.warn(message);
break;
}
default: throw new Error(message);
}
}
//#endregion
//#region src/converters/convertAction/convertAction.ts
/**
* Converts any supported Valibot action to the JSON Schema format.
*
* @param jsonSchema The JSON Schema object.
* @param valibotAction The Valibot action object.
* @param config The conversion configuration.
*
* @returns The converted JSON Schema.
*/
function convertAction(jsonSchema, valibotAction, config) {
if (config?.ignoreActions?.includes(valibotAction.type)) return jsonSchema;
let errors;
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") errors = addError(errors, `The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`);
jsonSchema.maxLength = 0;
}
break;
}
case "entries": {
jsonSchema.minProperties = valibotAction.requirement;
jsonSchema.maxProperties = valibotAction.requirement;
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") errors = addError(errors, `The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`);
jsonSchema.minLength = valibotAction.requirement;
jsonSchema.maxLength = valibotAction.requirement;
}
break;
}
case "max_entries": {
jsonSchema.maxProperties = valibotAction.requirement;
break;
}
case "max_length": {
if (jsonSchema.type === "array") jsonSchema.maxItems = valibotAction.requirement;
else {
if (jsonSchema.type !== "string") errors = addError(errors, `The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`);
jsonSchema.maxLength = valibotAction.requirement;
}
break;
}
case "max_value": {
if (jsonSchema.type !== "number") errors = addError(errors, `The "max_value" action is not supported on type "${jsonSchema.type}".`);
jsonSchema.maximum = valibotAction.requirement;
break;
}
case "metadata": {
if (typeof valibotAction.metadata.title === "string") jsonSchema.title = valibotAction.metadata.title;
if (typeof valibotAction.metadata.description === "string") jsonSchema.description = valibotAction.metadata.description;
if (Array.isArray(valibotAction.metadata.examples)) jsonSchema.examples = valibotAction.metadata.examples;
break;
}
case "min_entries": {
jsonSchema.minProperties = valibotAction.requirement;
break;
}
case "min_length": {
if (jsonSchema.type === "array") jsonSchema.minItems = valibotAction.requirement;
else {
if (jsonSchema.type !== "string") errors = addError(errors, `The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`);
jsonSchema.minLength = valibotAction.requirement;
}
break;
}
case "min_value": {
if (jsonSchema.type !== "number") errors = addError(errors, `The "min_value" action is not supported on type "${jsonSchema.type}".`);
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") errors = addError(errors, `The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`);
jsonSchema.minLength = 1;
}
break;
}
case "regex": {
if (valibotAction.requirement.flags) errors = addError(errors, "RegExp flags are not supported by JSON Schema.");
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: errors = addError(errors, `The "${valibotAction.type}" action cannot be converted to JSON Schema.`);
}
if (config?.overrideAction) {
const actionOverride = config.overrideAction({
valibotAction,
jsonSchema,
errors
});
if (actionOverride) return { ...actionOverride };
}
if (errors) for (const message of errors) handleError(message, config);
return jsonSchema;
}
//#endregion
//#region src/converters/convertSchema/convertSchema.ts
/**
* Flattens a Valibot pipe by recursively expanding nested pipes.
*
* @param pipe The pipeline to flatten.
*
* @returns A flat pipeline.
*/
function flattenPipe(pipe) {
return pipe.flatMap((item) => "pipe" in item ? flattenPipe(item.pipe) : item);
}
let refCount = 0;
/**
* Converts any supported Valibot schema to the JSON Schema format.
*
* @param jsonSchema The JSON Schema object.
* @param valibotSchema The Valibot schema object.
* @param config The conversion configuration.
* @param context The conversion context.
* @param skipRef Whether to skip using a reference.
*
* @returns The converted JSON Schema.
*/
function convertSchema(jsonSchema, valibotSchema, config, context, skipRef = false) {
if (!skipRef) {
const referenceId = context.referenceMap.get(valibotSchema);
if (referenceId) {
jsonSchema.$ref = `#/$defs/${referenceId}`;
if (config?.overrideRef) {
const refOverride = config.overrideRef({
...context,
referenceId,
valibotSchema,
jsonSchema
});
if (refOverride) jsonSchema.$ref = refOverride;
}
return jsonSchema;
}
}
if ("pipe" in valibotSchema) {
const flatPipe = flattenPipe(valibotSchema.pipe);
let startIndex = 0;
let stopIndex = flatPipe.length - 1;
if (config?.typeMode === "input") {
const inputStopIndex = flatPipe.slice(1).findIndex((item) => item.kind === "schema" || item.kind === "transformation" && (item.type === "find_item" || item.type === "parse_json" || item.type === "raw_transform" || item.type === "reduce_items" || item.type === "stringify_json" || item.type === "transform"));
if (inputStopIndex !== -1) stopIndex = inputStopIndex;
} else if (config?.typeMode === "output") {
const outputStartIndex = flatPipe.findLastIndex((item) => item.kind === "schema");
if (outputStartIndex !== -1) startIndex = outputStartIndex;
}
for (let index = startIndex; index <= stopIndex; index++) {
const valibotPipeItem = flatPipe[index];
if (valibotPipeItem.kind === "schema") {
if (index > startIndex) handleError("Set the \"typeMode\" config to \"input\" or \"output\" to convert pipelines with multiple schemas.", config);
jsonSchema = convertSchema(jsonSchema, valibotPipeItem, config, context, true);
} else jsonSchema = convertAction(jsonSchema, valibotPipeItem, config);
}
return jsonSchema;
}
let errors;
switch (valibotSchema.type) {
case "boolean": {
jsonSchema.type = "boolean";
break;
}
case "null": {
jsonSchema.type = "null";
break;
}
case "number": {
jsonSchema.type = "number";
break;
}
case "string": {
jsonSchema.type = "string";
break;
}
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 = [];
jsonSchema.minItems = valibotSchema.items.length;
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 if (valibotSchema.type === "strict_tuple") jsonSchema.additionalItems = false;
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) errors = addError(errors, "The \"record\" schema with a schema for the key that contains a \"pipe\" cannot be converted to JSON Schema.");
if (valibotSchema.key.type !== "string") errors = addError(errors, `The "record" schema with the "${valibotSchema.key.type}" schema for the key cannot be converted to JSON Schema.`);
jsonSchema.type = "object";
jsonSchema.additionalProperties = convertSchema({}, valibotSchema.value, config, context);
break;
}
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") errors = addError(errors, "The value of the \"literal\" schema is not JSON compatible.");
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")) errors = addError(errors, "An option of the \"picklist\" schema is not JSON compatible.");
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 referenceId = context.referenceMap.get(wrappedValibotSchema);
if (!referenceId) {
referenceId = `${refCount++}`;
context.referenceMap.set(wrappedValibotSchema, referenceId);
context.definitions[referenceId] = convertSchema({}, wrappedValibotSchema, config, context, true);
}
jsonSchema.$ref = `#/$defs/${referenceId}`;
if (config?.overrideRef) {
const refOverride = config.overrideRef({
...context,
referenceId,
valibotSchema: wrappedValibotSchema,
jsonSchema
});
if (refOverride) jsonSchema.$ref = refOverride;
}
break;
}
default: errors = addError(errors, `The "${valibotSchema.type}" schema cannot be converted to JSON Schema.`);
}
if (config?.overrideSchema) {
const schemaOverride = config.overrideSchema({
...context,
referenceId: context.referenceMap.get(valibotSchema),
valibotSchema,
jsonSchema,
errors
});
if (schemaOverride) return { ...schemaOverride };
}
if (errors) for (const message of errors) handleError(message, config);
return jsonSchema;
}
//#endregion
//#region src/storages/globalDefs/globalDefs.ts
let store;
/**
* Adds new definitions to the global schema definitions.
*
* @param definitions The schema definitions.
*
* @beta
*/
function addGlobalDefs(definitions) {
store = {
...store ?? {},
...definitions
};
}
/**
* Returns the current global schema definitions.
*
* @returns The schema definitions.
*
* @beta
*/
function getGlobalDefs() {
return store;
}
//#endregion
//#region src/functions/toJsonSchema/toJsonSchema.ts
/**
* Converts a Valibot schema to the JSON Schema format.
*
* @param schema The Valibot schema object.
* @param config The JSON Schema configuration.
*
* @returns The converted JSON Schema.
*/
function toJsonSchema(schema, config) {
const context = {
definitions: {},
referenceMap: new Map(),
getterMap: new Map()
};
const definitions = config?.definitions ?? getGlobalDefs();
if (definitions) {
for (const key in definitions) context.referenceMap.set(definitions[key], key);
for (const key in definitions) context.definitions[key] = convertSchema({}, definitions[key], config, context, true);
}
const jsonSchema = convertSchema({ $schema: "http://json-schema.org/draft-07/schema#" }, schema, config, context);
if (context.referenceMap.size) jsonSchema.$defs = context.definitions;
return jsonSchema;
}
//#endregion
//#region src/functions/toJsonSchemaDefs/toJsonSchemaDefs.ts
/**
* Converts Valibot schema definitions to JSON Schema definitions.
*
* @param definitions The Valibot schema definitions.
* @param config The JSON Schema configuration.
*
* @returns The converted JSON Schema definitions.
*/
function toJsonSchemaDefs(definitions, config) {
const context = {
definitions: {},
referenceMap: new Map(),
getterMap: new Map()
};
for (const key in definitions) context.referenceMap.set(definitions[key], key);
for (const key in definitions) context.definitions[key] = convertSchema({}, definitions[key], config, context, true);
return context.definitions;
}
//#endregion
export { addGlobalDefs, getGlobalDefs, toJsonSchema, toJsonSchemaDefs };