@gcornut/valibot-json-schema
Version:
> [!CAUTION] > This project is now deprecated as Valibot now has an official JSON schema converter in the [@valibot/to-json-schema](https://github.com/fabian-hiller/valibot/tree/main/packages/to-json-schema). > The command line interface is moved to the p
341 lines (327 loc) • 12.7 kB
JavaScript
// src/extension/withJSONSchemaFeatures.ts
var JSON_SCHEMA_FEATURES_KEY = "__json_schema_features";
function withJSONSchemaFeatures(schema, features) {
return Object.assign(schema, { [JSON_SCHEMA_FEATURES_KEY]: features });
}
function getJSONSchemaFeatures(schema) {
return schema[JSON_SCHEMA_FEATURES_KEY];
}
// src/extension/assignExtraJSONSchemaFeatures.ts
function assignExtraJSONSchemaFeatures(schema, converted) {
const jsonSchemaFeatures = getJSONSchemaFeatures(schema);
if (jsonSchemaFeatures) {
Object.assign(converted, jsonSchemaFeatures);
}
}
// src/utils/assert.ts
function assert(value, predicate, message) {
if (!predicate(value)) throw new Error(message.replace("%", String(value)));
return value;
}
// src/utils/json-schema.ts
var $schema = "http://json-schema.org/draft-07/schema#";
function isJSONLiteral(value) {
return typeof value === "number" && !Number.isNaN(value) || typeof value === "string" || typeof value === "boolean" || value === null;
}
var assertJSONLiteral = (v) => assert(v, isJSONLiteral, "Unsupported literal value type: %");
// src/toJSONSchema/schemas.ts
import {
getDefault
} from "valibot";
// src/utils/isEqual.ts
function isEqual(obj1, obj2) {
if (obj1 === obj2) return true;
if (typeof obj1 === "object" && typeof obj2 === "object") {
const keys1 = Object.keys(obj1);
const keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
return keys1.every((key1) => isEqual(obj1[key1], obj2[key1]));
}
return false;
}
// src/utils/valibot.ts
function isSchemaType(type) {
return (schema) => {
return !!schema && schema.type === type;
};
}
var isNullishSchema = isSchemaType("nullish");
var isOptionalSchema = isSchemaType("optional");
var isStringSchema = isSchemaType("string");
var isNeverSchema = isSchemaType("never");
// src/toJSONSchema/toDefinitionURI.ts
var toDefinitionURI = (name) => `#/definitions/${name}`;
// src/toJSONSchema/schemas.ts
var SCHEMA_CONVERTERS = {
any: () => ({}),
// Core types
null: () => ({ const: null }),
literal: ({ literal }) => ({ const: assertJSONLiteral(literal) }),
number: () => ({ type: "number" }),
string: () => ({ type: "string" }),
boolean: () => ({ type: "boolean" }),
// Compositions
optional: (schema, convert) => {
const output = convert(schema.wrapped);
const defaultValue = getDefault(schema);
if (defaultValue !== void 0) output.default = defaultValue;
return output;
},
nullish: (schema, convert) => {
const output = { anyOf: [{ const: null }, convert(schema.wrapped)] };
const defaultValue = getDefault(schema);
if (defaultValue !== void 0) output.default = defaultValue;
return output;
},
nullable: (schema, convert) => {
const output = { anyOf: [{ const: null }, convert(schema.wrapped)] };
const defaultValue = getDefault(schema);
if (defaultValue !== void 0) output.default = defaultValue;
return output;
},
picklist: ({ options }) => ({ enum: options.map(assertJSONLiteral) }),
enum: (options) => ({ enum: Object.values(options.enum).map(assertJSONLiteral) }),
union: ({ options }, convert) => ({ anyOf: options.map(convert) }),
intersect: ({ options }, convert) => ({ allOf: options.map(convert) }),
// Complex types
array: ({ item }, convert) => ({ type: "array", items: convert(item) }),
tuple_with_rest({ items: originalItems, rest }, convert) {
const minItems = originalItems.length;
let maxItems;
let items = originalItems.map(convert);
let additionalItems;
if (isNeverSchema(rest)) {
maxItems = minItems;
} else if (rest) {
const restItems = convert(rest);
if (items.length === 1 && isEqual(items[0], restItems)) {
items = items[0];
} else {
additionalItems = restItems;
}
}
return {
type: "array",
items,
...additionalItems && { additionalItems },
...minItems && { minItems },
...maxItems && { maxItems }
};
},
strict_tuple({ items: originalItems }, convert) {
const items = originalItems.map(convert);
return { type: "array", items, minItems: items.length, maxItems: items.length };
},
tuple({ items: originalItems }, convert, context) {
const items = originalItems.map(convert);
return { type: "array", items, minItems: items.length };
},
object_with_rest({ entries, rest }, convert, context) {
const properties = {};
const required = [];
for (const [propKey, propValue] of Object.entries(entries)) {
const propSchema = propValue;
if (!isOptionalSchema(propSchema) && !isNullishSchema(propSchema)) {
required.push(propKey);
}
properties[propKey] = convert(propSchema);
assignExtraJSONSchemaFeatures(propValue, properties[propKey]);
}
let additionalProperties;
if (rest) {
additionalProperties = isNeverSchema(rest) ? false : convert(rest);
} else if (context.strictObjectTypes) {
additionalProperties = false;
}
const output = { type: "object", properties };
if (additionalProperties !== void 0) output.additionalProperties = additionalProperties;
if (required.length) output.required = required;
return output;
},
object(schema, convert, context) {
return SCHEMA_CONVERTERS.object_with_rest(schema, convert, context);
},
strict_object(schema, convert, context) {
const object = SCHEMA_CONVERTERS.object_with_rest(schema, convert, context);
return { ...object, additionalProperties: false };
},
record({ key, value }, convert) {
assert(key, isStringSchema, "Unsupported record key type: %");
return { type: "object", additionalProperties: convert(value) };
},
lazy(schema, _, context) {
const nested = schema.getter({});
const defName = context.defNameMap.get(nested);
if (!defName) {
throw new Error("Type inside lazy schema must be provided in the definitions");
}
return { $ref: toDefinitionURI(defName) };
},
date(_, __, context) {
if (!context.dateStrategy) {
throw new Error('The "dateStrategy" option must be set to handle date validators');
}
switch (context.dateStrategy) {
case "integer":
return { type: "integer", format: "unix-time" };
case "string":
return { type: "string", format: "date-time" };
}
},
undefined(_, __, context) {
if (!context.undefinedStrategy) {
throw new Error('The "undefinedStrategy" option must be set to handle the `undefined` schema');
}
switch (context.undefinedStrategy) {
case "any":
return {};
case "null":
return { type: "null" };
}
},
bigint(_, __, context) {
if (!context.bigintStrategy) {
throw new Error('The "bigintStrategy" option must be set to handle `bigint` validators');
}
switch (context.bigintStrategy) {
case "integer":
return { type: "integer", format: "int64" };
case "string":
return { type: "string" };
}
},
variant({ options }, ...args) {
return SCHEMA_CONVERTERS.union({ options }, ...args);
}
};
// src/toJSONSchema/actions/metadata.ts
var METADATA_BY_TYPE = {
description: ({ description }) => ({ description }),
"@gcornut/to-json-schema/json_schema_metadata": ({ metadata }) => metadata
};
// src/toJSONSchema/actions/validations.ts
function asDateRequirement(type, requirement, context) {
assert(requirement, () => context.dateStrategy === "integer", `${type} validation is only available with 'integer' date strategy`);
assert(requirement, (r) => r instanceof Date, `Non-date value used for ${type} validation`);
return requirement.getTime();
}
var VALIDATION_BY_SCHEMA = {
array: {
length: ({ requirement }) => ({ minItems: requirement, maxItems: requirement }),
min_length: ({ requirement }) => ({ minItems: requirement }),
max_length: ({ requirement }) => ({ maxItems: requirement })
},
string: {
value: ({ requirement }) => ({ const: requirement }),
length: ({ requirement }) => ({ minLength: requirement, maxLength: requirement }),
min_length: ({ requirement }) => ({ minLength: requirement }),
max_length: ({ requirement }) => ({ maxLength: requirement }),
// TODO: validate RegExp features are compatible with json schema ?
regex: ({ requirement }) => ({ pattern: requirement.source }),
email: () => ({ format: "email" }),
iso_date: () => ({ format: "date" }),
iso_timestamp: () => ({ format: "date-time" }),
ipv4: () => ({ format: "ipv4" }),
ipv6: () => ({ format: "ipv6" }),
uuid: () => ({ format: "uuid" })
},
number: {
value: ({ requirement }) => ({ const: requirement }),
min_value: ({ requirement }) => ({ minimum: requirement }),
max_value: ({ requirement }) => ({ maximum: requirement }),
multiple_of: ({ requirement }) => ({ multipleOf: requirement }),
integer: () => ({ type: "integer" })
},
boolean: {
value: ({ requirement }) => ({ const: requirement })
},
date: {
value: ({ requirement }, context) => ({ const: asDateRequirement("value", requirement, context) }),
min_value: ({ requirement }, context) => ({ minimum: asDateRequirement("minValue", requirement, context) }),
max_value: ({ requirement }, context) => ({ maximum: asDateRequirement("maxValue", requirement, context) })
}
};
// src/toJSONSchema/actions/convertPipe.ts
function convertPipe(schemaType, pipe, context) {
const [schema, ...pipeItems] = pipe || [];
if (!schema) return {};
const childPipe = convertPipe(schemaType, schema == null ? void 0 : schema.pipe, context);
function convertPipeItem(def, validation) {
var _a, _b, _c;
const validationType = validation.type;
const validationConverter = ((_b = (_a = context.customValidationConversion) == null ? void 0 : _a[schemaType]) == null ? void 0 : _b[validationType]) || ((_c = VALIDATION_BY_SCHEMA[schemaType]) == null ? void 0 : _c[validationType]) || METADATA_BY_TYPE[validationType];
if (!validationConverter && context.ignoreUnknownValidation) return {};
assert(validationConverter, Boolean, `Unsupported valibot validation \`${validationType}\` for schema \`${schemaType}\``);
const converted = validationConverter(validation, context);
return Object.assign(def, converted);
}
return pipeItems.reduce(convertPipeItem, childPipe);
}
// src/toJSONSchema/index.ts
function getDefNameMap(definitions = {}) {
const map = /* @__PURE__ */ new Map();
for (const [name, definition] of Object.entries(definitions)) {
map.set(definition, name);
}
return map;
}
function createConverter(context) {
const definitions = {};
function converter(schema) {
var _a;
const defName = context.defNameMap.get(schema);
const defURI = defName && toDefinitionURI(defName);
if (defURI && defURI in definitions) {
return { $ref: defURI };
}
const schemaConverter = ((_a = context.customSchemaConversion) == null ? void 0 : _a[schema.type]) || SCHEMA_CONVERTERS[schema.type];
assert(schemaConverter, Boolean, `Unsupported valibot schema: ${(schema == null ? void 0 : schema.type) || schema}`);
let converted = schemaConverter(schema, converter, context) || {};
const convertedValidation = convertPipe(schema.type, schema.pipe, context);
converted = { ...converted, ...convertedValidation };
assignExtraJSONSchemaFeatures(schema, converted);
if (defURI) {
definitions[defName] = converted;
return { $ref: defURI };
}
return converted;
}
return { definitions, converter };
}
function toJSONSchema(options) {
const { schema, definitions: inputDefinitions, ...more } = options;
const defNameMap = getDefNameMap(inputDefinitions);
const { definitions, converter } = createConverter({ defNameMap, ...more });
if (!schema && !inputDefinitions) {
throw new Error("No main schema or definitions provided.");
}
if (inputDefinitions) {
Object.values(inputDefinitions).forEach(converter);
}
const mainConverted = schema && converter(schema);
const mainDefName = schema && defNameMap.get(schema);
const out = { $schema };
if (mainDefName) {
out.$ref = toDefinitionURI(mainDefName);
} else {
Object.assign(out, mainConverted);
}
if (Object.keys(definitions).length) {
out.definitions = definitions;
}
return out;
}
// src/extension/jsonSchemaMetadata.ts
function jsonSchemaMetadata(metadata) {
return {
kind: "metadata",
type: "@gcornut/to-json-schema/json_schema_metadata",
reference: jsonSchemaMetadata,
metadata
};
}
export {
jsonSchemaMetadata,
toJSONSchema,
withJSONSchemaFeatures
};