930 lines
40 KiB
Plaintext
930 lines
40 KiB
Plaintext
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.allProcessors = exports.lazyProcessor = exports.optionalProcessor = exports.promiseProcessor = exports.readonlyProcessor = exports.pipeProcessor = exports.catchProcessor = exports.prefaultProcessor = exports.defaultProcessor = exports.nonoptionalProcessor = exports.nullableProcessor = exports.recordProcessor = exports.tupleProcessor = exports.intersectionProcessor = exports.unionProcessor = exports.objectProcessor = exports.arrayProcessor = exports.setProcessor = exports.mapProcessor = exports.transformProcessor = exports.functionProcessor = exports.customProcessor = exports.successProcessor = exports.fileProcessor = exports.templateLiteralProcessor = exports.nanProcessor = exports.literalProcessor = exports.enumProcessor = exports.dateProcessor = exports.unknownProcessor = exports.anyProcessor = exports.neverProcessor = exports.voidProcessor = exports.undefinedProcessor = exports.nullProcessor = exports.symbolProcessor = exports.bigintProcessor = exports.booleanProcessor = exports.numberProcessor = exports.stringProcessor = void 0;
|
|
exports.aggregateChecks = aggregateChecks;
|
|
exports.toJSONSchema = toJSONSchema;
|
|
const regexes = __importStar(require("./regexes.cjs"));
|
|
const schemas_js_1 = require("./schemas.cjs");
|
|
const to_json_schema_js_1 = require("./to-json-schema.cjs");
|
|
const util_js_1 = require("./util.cjs");
|
|
const narrowMin = (agg, key, value) => {
|
|
if (agg[key] === undefined || value > agg[key])
|
|
agg[key] = value;
|
|
};
|
|
const narrowMax = (agg, key, value) => {
|
|
if (agg[key] === undefined || value < agg[key])
|
|
agg[key] = value;
|
|
};
|
|
const narrowBoth = (agg, value) => {
|
|
narrowMin(agg, "minimum", value);
|
|
narrowMax(agg, "maximum", value);
|
|
};
|
|
const addDivisor = (agg, value) => {
|
|
agg.multipleOf ?? (agg.multipleOf = []);
|
|
if (!agg.multipleOf.includes(value))
|
|
agg.multipleOf.push(value);
|
|
};
|
|
const addPattern = (agg, pattern) => {
|
|
agg.patterns ?? (agg.patterns = new Set());
|
|
agg.patterns.add(pattern);
|
|
};
|
|
const intersectMime = (agg, mime) => {
|
|
agg.mime = agg.mime ? agg.mime.filter((m) => mime.includes(m)) : [...mime];
|
|
};
|
|
// last-wins, matching the bag's historical write order; the flag keeps an integer format from being lost to a later float one
|
|
const setFormat = (agg, format) => {
|
|
agg.format = format;
|
|
if (format.includes("int"))
|
|
agg.isInt = true;
|
|
};
|
|
const minContributor = (agg, def) => narrowMin(agg, "minimum", def.minimum);
|
|
const maxContributor = (agg, def) => narrowMax(agg, "maximum", def.maximum);
|
|
const formatContributor = (ranges) => (agg, def) => {
|
|
setFormat(agg, def.format);
|
|
const [minimum, maximum] = ranges[def.format];
|
|
narrowMin(agg, "minimum", minimum);
|
|
narrowMax(agg, "maximum", maximum);
|
|
};
|
|
const contributors = {
|
|
greater_than: (agg, def) => narrowMin(agg, def.inclusive ? "minimum" : "exclusiveMinimum", def.value),
|
|
less_than: (agg, def) => narrowMax(agg, def.inclusive ? "maximum" : "exclusiveMaximum", def.value),
|
|
multiple_of: (agg, def) => addDivisor(agg, def.value),
|
|
number_format: formatContributor(util_js_1.NUMBER_FORMAT_RANGES),
|
|
bigint_format: formatContributor(util_js_1.BIGINT_FORMAT_RANGES),
|
|
min_length: minContributor,
|
|
max_length: maxContributor,
|
|
length_equals: (agg, def) => narrowBoth(agg, def.length),
|
|
min_size: minContributor,
|
|
max_size: maxContributor,
|
|
size_equals: (agg, def) => narrowBoth(agg, def.size),
|
|
string_format: (agg, def) => {
|
|
setFormat(agg, def.format);
|
|
if (def.pattern)
|
|
addPattern(agg, def.pattern);
|
|
if (def.format === "base64" || def.format === "base64url")
|
|
agg.contentEncoding = def.format;
|
|
if (def.local || def.precision === -1)
|
|
agg.laxFormat = true;
|
|
},
|
|
mime_type: (agg, def) => intersectMime(agg, def.mime),
|
|
};
|
|
function aggregateChecks(schema) {
|
|
const agg = {};
|
|
const def = schema._zod.def;
|
|
// a format schema is its own first check, same rule as $ZodType init
|
|
const list = schema._zod.traits.has("$ZodCheck")
|
|
? [schema, ...(def.checks ?? [])]
|
|
: (def.checks ?? []);
|
|
for (const ch of list)
|
|
contributors[ch._zod.def.check]?.(agg, ch._zod.def);
|
|
// reconcile with the bag so third-party onattach contributions still land; first-party residue is never tighter than the fold, so merging it back is idempotent for one and additive for the other
|
|
const bag = schema._zod.bag;
|
|
if (bag.minimum !== undefined)
|
|
narrowMin(agg, "minimum", bag.minimum);
|
|
if (bag.exclusiveMinimum !== undefined)
|
|
narrowMin(agg, "exclusiveMinimum", bag.exclusiveMinimum);
|
|
if (bag.maximum !== undefined)
|
|
narrowMax(agg, "maximum", bag.maximum);
|
|
if (bag.exclusiveMaximum !== undefined)
|
|
narrowMax(agg, "exclusiveMaximum", bag.exclusiveMaximum);
|
|
if (bag.multipleOf !== undefined)
|
|
addDivisor(agg, bag.multipleOf);
|
|
if (bag.format !== undefined) {
|
|
agg.format ?? (agg.format = bag.format);
|
|
if (bag.format.includes("int"))
|
|
agg.isInt = true;
|
|
}
|
|
if (bag.mime)
|
|
intersectMime(agg, bag.mime);
|
|
for (const pattern of bag.patterns ?? [])
|
|
addPattern(agg, pattern);
|
|
return agg;
|
|
}
|
|
const formatMap = {
|
|
guid: "uuid",
|
|
url: "uri",
|
|
datetime: "date-time",
|
|
json_string: "json-string",
|
|
regex: "", // do not set
|
|
};
|
|
// ==================== SIMPLE TYPE PROCESSORS ====================
|
|
// the runtime patterns are lax so parse paths never overflow the regex stack; the emitted schema swaps in the exact block forms, which zod itself never executes
|
|
const exactPatterns = new Map([
|
|
[schemas_js_1.base64Charset, regexes.base64],
|
|
[schemas_js_1.base64urlCharset, regexes.base64url],
|
|
]);
|
|
const exactPattern = (p) => exactPatterns.get(p) ?? p;
|
|
const stringProcessor = (schema, ctx, _json, _params) => {
|
|
const json = _json;
|
|
json.type = "string";
|
|
const { minimum, maximum, format, patterns, contentEncoding, laxFormat } = aggregateChecks(schema);
|
|
if (typeof minimum === "number")
|
|
json.minLength = minimum;
|
|
if (typeof maximum === "number")
|
|
json.maxLength = maximum;
|
|
// custom pattern overrides format
|
|
if (format) {
|
|
json.format = formatMap[format] ?? format;
|
|
if (json.format === "")
|
|
delete json.format; // empty format is not valid
|
|
// `z.iso.time()` is never full-time, and `laxFormat` carries the datetime shapes that also accept what their keyword forbids
|
|
if (format === "time" || laxFormat) {
|
|
delete json.format;
|
|
}
|
|
}
|
|
if (contentEncoding)
|
|
json.contentEncoding = contentEncoding;
|
|
if (patterns && patterns.size > 0) {
|
|
const patternList = [...patterns].map(exactPattern);
|
|
if (patternList.length === 1)
|
|
json.pattern = patternList[0].source;
|
|
else if (patternList.length > 1) {
|
|
json.allOf = [
|
|
...patternList.map((regex) => ({
|
|
...(ctx.target === "draft-07" || ctx.target === "draft-04" || ctx.target === "openapi-3.0"
|
|
? { type: "string" }
|
|
: {}),
|
|
pattern: regex.source,
|
|
})),
|
|
];
|
|
}
|
|
}
|
|
};
|
|
exports.stringProcessor = stringProcessor;
|
|
const numberProcessor = (schema, ctx, _json, params) => {
|
|
const json = _json;
|
|
const { minimum, maximum, multipleOf, exclusiveMaximum, exclusiveMinimum, isInt } = aggregateChecks(schema);
|
|
json.type = isInt ? "integer" : "number";
|
|
// when both minimum and exclusiveMinimum exist, pick the more restrictive one
|
|
const exMin = typeof exclusiveMinimum === "number" && exclusiveMinimum >= (minimum ?? Number.NEGATIVE_INFINITY);
|
|
const exMax = typeof exclusiveMaximum === "number" && exclusiveMaximum <= (maximum ?? Number.POSITIVE_INFINITY);
|
|
const legacy = ctx.target === "draft-04" || ctx.target === "openapi-3.0";
|
|
if (exMin) {
|
|
if (legacy) {
|
|
json.minimum = exclusiveMinimum;
|
|
json.exclusiveMinimum = true;
|
|
}
|
|
else {
|
|
json.exclusiveMinimum = exclusiveMinimum;
|
|
}
|
|
}
|
|
else if (typeof minimum === "number") {
|
|
json.minimum = minimum;
|
|
}
|
|
if (exMax) {
|
|
if (legacy) {
|
|
json.maximum = exclusiveMaximum;
|
|
json.exclusiveMaximum = true;
|
|
}
|
|
else {
|
|
json.exclusiveMaximum = exclusiveMaximum;
|
|
}
|
|
}
|
|
else if (typeof maximum === "number") {
|
|
json.maximum = maximum;
|
|
}
|
|
if (multipleOf) {
|
|
// JSON Schema requires a divisor strictly greater than zero, and a non-finite one does not survive JSON at all. A negative divisor accepts exactly what its absolute value accepts, so it still maps; zero, NaN and Infinity have no keyword form.
|
|
const divisors = new Set();
|
|
for (const divisor of multipleOf) {
|
|
if (Number.isFinite(divisor) && divisor !== 0)
|
|
divisors.add(Math.abs(divisor));
|
|
else
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, `A multipleOf divisor of ${divisor} cannot be represented in JSON Schema`);
|
|
}
|
|
// chained divisors are a conjunction the keyword cannot carry alone, so extras ride an allOf, same as stacked patterns
|
|
const [first, ...rest] = divisors;
|
|
if (first !== undefined)
|
|
json.multipleOf = first;
|
|
if (rest.length)
|
|
json.allOf = [...(json.allOf ?? []), ...rest.map((m) => ({ multipleOf: m }))];
|
|
}
|
|
};
|
|
exports.numberProcessor = numberProcessor;
|
|
const booleanProcessor = (_schema, _ctx, json, _params) => {
|
|
json.type = "boolean";
|
|
};
|
|
exports.booleanProcessor = booleanProcessor;
|
|
const bigintProcessor = (schema, ctx, json, params) => {
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "BigInt cannot be represented in JSON Schema");
|
|
};
|
|
exports.bigintProcessor = bigintProcessor;
|
|
const symbolProcessor = (schema, ctx, json, params) => {
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "Symbols cannot be represented in JSON Schema");
|
|
};
|
|
exports.symbolProcessor = symbolProcessor;
|
|
const nullProcessor = (_schema, ctx, json, _params) => {
|
|
if (ctx.target === "openapi-3.0") {
|
|
json.type = "string";
|
|
json.nullable = true;
|
|
json.enum = [null];
|
|
}
|
|
else {
|
|
json.type = "null";
|
|
}
|
|
};
|
|
exports.nullProcessor = nullProcessor;
|
|
const undefinedProcessor = (schema, ctx, json, params) => {
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "Undefined cannot be represented in JSON Schema");
|
|
};
|
|
exports.undefinedProcessor = undefinedProcessor;
|
|
const voidProcessor = (schema, ctx, json, params) => {
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "Void cannot be represented in JSON Schema");
|
|
};
|
|
exports.voidProcessor = voidProcessor;
|
|
const neverProcessor = (_schema, _ctx, json, _params) => {
|
|
json.not = {};
|
|
};
|
|
exports.neverProcessor = neverProcessor;
|
|
const anyProcessor = (_schema, _ctx, _json, _params) => {
|
|
// empty schema accepts anything
|
|
};
|
|
exports.anyProcessor = anyProcessor;
|
|
const unknownProcessor = (_schema, _ctx, _json, _params) => {
|
|
// empty schema accepts anything
|
|
};
|
|
exports.unknownProcessor = unknownProcessor;
|
|
const dateProcessor = (schema, ctx, json, params) => {
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "Date cannot be represented in JSON Schema");
|
|
};
|
|
exports.dateProcessor = dateProcessor;
|
|
const enumProcessor = (schema, _ctx, json, _params) => {
|
|
const def = schema._zod.def;
|
|
const values = (0, util_js_1.getEnumValues)(def.entries);
|
|
// an empty enum accepts nothing, same as z.never()
|
|
if (values.length === 0) {
|
|
json.not = {};
|
|
return;
|
|
}
|
|
// Number enums can have both string and number values
|
|
if (values.every((v) => typeof v === "number"))
|
|
json.type = "number";
|
|
if (values.every((v) => typeof v === "string"))
|
|
json.type = "string";
|
|
json.enum = values;
|
|
};
|
|
exports.enumProcessor = enumProcessor;
|
|
const literalProcessor = (schema, ctx, json, params) => {
|
|
const def = schema._zod.def;
|
|
// a literal with no values accepts nothing, same as z.never()
|
|
if (def.values.length === 0) {
|
|
json.not = {};
|
|
return;
|
|
}
|
|
const vals = [];
|
|
for (const val of def.values) {
|
|
if (val === undefined) {
|
|
// a custom schema replaces the whole literal, so there is nothing left to accumulate
|
|
if ((0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "Literal `undefined` cannot be represented in JSON Schema"))
|
|
return;
|
|
// otherwise do not add to vals
|
|
}
|
|
else if (typeof val === "bigint") {
|
|
if ((0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "BigInt literals cannot be represented in JSON Schema"))
|
|
return;
|
|
vals.push(Number(val));
|
|
}
|
|
else {
|
|
vals.push(val);
|
|
}
|
|
}
|
|
if (vals.length === 0) {
|
|
// do nothing (an undefined literal was stripped)
|
|
}
|
|
else if (vals.length === 1) {
|
|
const val = vals[0];
|
|
json.type = val === null ? "null" : typeof val;
|
|
if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") {
|
|
json.enum = [val];
|
|
}
|
|
else {
|
|
json.const = val;
|
|
}
|
|
}
|
|
else {
|
|
if (vals.every((v) => typeof v === "number"))
|
|
json.type = "number";
|
|
if (vals.every((v) => typeof v === "string"))
|
|
json.type = "string";
|
|
if (vals.every((v) => typeof v === "boolean"))
|
|
json.type = "boolean";
|
|
if (vals.every((v) => v === null))
|
|
json.type = "null";
|
|
json.enum = vals;
|
|
}
|
|
};
|
|
exports.literalProcessor = literalProcessor;
|
|
const nanProcessor = (schema, ctx, json, params) => {
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "NaN cannot be represented in JSON Schema");
|
|
};
|
|
exports.nanProcessor = nanProcessor;
|
|
const templateLiteralProcessor = (schema, _ctx, json, _params) => {
|
|
const _json = json;
|
|
const pattern = schema._zod.pattern;
|
|
if (!pattern)
|
|
throw new Error("Pattern not found in template literal");
|
|
_json.type = "string";
|
|
_json.pattern = pattern.source;
|
|
};
|
|
exports.templateLiteralProcessor = templateLiteralProcessor;
|
|
const fileProcessor = (schema, _ctx, json, _params) => {
|
|
const _json = json;
|
|
_json.type = "string";
|
|
_json.format = "binary";
|
|
_json.contentEncoding = "binary";
|
|
const { minimum, maximum, mime } = aggregateChecks(schema);
|
|
if (minimum !== undefined)
|
|
_json.minLength = minimum;
|
|
if (maximum !== undefined)
|
|
_json.maxLength = maximum;
|
|
if (!mime)
|
|
return;
|
|
// an empty intersection means the mime checks share no value, so nothing passes at runtime; `anyOf` must be non-empty, so the false schema is `not: {}`
|
|
if (mime.length === 0)
|
|
_json.not = {};
|
|
else if (mime.length === 1)
|
|
_json.contentMediaType = mime[0];
|
|
// only contentMediaType differs, so the shared props stay at the root
|
|
else
|
|
_json.anyOf = mime.map((m) => ({ contentMediaType: m }));
|
|
};
|
|
exports.fileProcessor = fileProcessor;
|
|
const successProcessor = (_schema, _ctx, json, _params) => {
|
|
json.type = "boolean";
|
|
};
|
|
exports.successProcessor = successProcessor;
|
|
const customProcessor = (schema, ctx, json, params) => {
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "Custom types cannot be represented in JSON Schema");
|
|
};
|
|
exports.customProcessor = customProcessor;
|
|
const functionProcessor = (schema, ctx, json, params) => {
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "Function types cannot be represented in JSON Schema");
|
|
};
|
|
exports.functionProcessor = functionProcessor;
|
|
const transformProcessor = (schema, ctx, json, params) => {
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "Transforms cannot be represented in JSON Schema");
|
|
};
|
|
exports.transformProcessor = transformProcessor;
|
|
const mapProcessor = (schema, ctx, json, params) => {
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "Map cannot be represented in JSON Schema");
|
|
};
|
|
exports.mapProcessor = mapProcessor;
|
|
const setProcessor = (schema, ctx, json, params) => {
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "Set cannot be represented in JSON Schema");
|
|
};
|
|
exports.setProcessor = setProcessor;
|
|
// ==================== COMPOSITE TYPE PROCESSORS ====================
|
|
const arrayProcessor = (schema, ctx, _json, params) => {
|
|
const json = _json;
|
|
const def = schema._zod.def;
|
|
const { minimum, maximum } = aggregateChecks(schema);
|
|
if (typeof minimum === "number")
|
|
json.minItems = minimum;
|
|
if (typeof maximum === "number")
|
|
json.maxItems = maximum;
|
|
json.type = "array";
|
|
json.items = (0, to_json_schema_js_1.processSchema)(def.element, ctx, {
|
|
...params,
|
|
path: [...params.path, "items"],
|
|
});
|
|
};
|
|
exports.arrayProcessor = arrayProcessor;
|
|
// Transform and catch set `optin = "optional"` at runtime so the parser lets them observe an
|
|
// absent key, but their declared input type stays required. An input JSON Schema describes the
|
|
// declared type, so resolve past them to the schema that actually carries the optionality.
|
|
// Used by both `objectProcessor` (for `required`) and `tupleProcessor` (for `minItems`); see
|
|
// wiki/optionality.md, "The JSON Schema emitter reads the *static* value".
|
|
function inputOptin(schema) {
|
|
const def = schema._zod.def;
|
|
if (def.type === "pipe" && def.in._zod.traits.has("$ZodTransform")) {
|
|
return inputOptin(def.out);
|
|
}
|
|
if (def.type === "catch") {
|
|
return inputOptin(def.innerType);
|
|
}
|
|
return schema._zod.optin;
|
|
}
|
|
const objectProcessor = (schema, ctx, _json, params) => {
|
|
const json = _json;
|
|
const def = schema._zod.def;
|
|
const shape = def.shape;
|
|
// dropping it while still emitting `additionalProperties: false` would emit a schema that rejects data this one requires
|
|
const symbolKeys = Object.getOwnPropertySymbols(shape);
|
|
if (symbolKeys.length &&
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "Symbol keys cannot be represented in JSON Schema")) {
|
|
return;
|
|
}
|
|
json.type = "object";
|
|
json.properties = {};
|
|
for (const key in shape) {
|
|
// assignProp so a __proto__ key becomes an own property instead of hitting the inherited setter on the plain {} we build into
|
|
(0, util_js_1.assignProp)(json.properties, key, (0, to_json_schema_js_1.processSchema)(shape[key], ctx, {
|
|
...params,
|
|
path: [...params.path, "properties", key],
|
|
}));
|
|
}
|
|
// required keys
|
|
const requiredKeys = [];
|
|
for (const key of Object.keys(shape)) {
|
|
const field = def.shape[key];
|
|
if (ctx.io === "input" ? inputOptin(field) === undefined : field._zod.optout === undefined) {
|
|
requiredKeys.push(key);
|
|
}
|
|
}
|
|
if (requiredKeys.length > 0) {
|
|
json.required = requiredKeys;
|
|
}
|
|
// catchall
|
|
if (def.catchall?._zod.def.type === "never") {
|
|
// strict
|
|
json.additionalProperties = false;
|
|
}
|
|
else if (!def.catchall) {
|
|
// regular
|
|
if (ctx.io === "output")
|
|
json.additionalProperties = false;
|
|
}
|
|
else if (def.catchall) {
|
|
json.additionalProperties = (0, to_json_schema_js_1.processSchema)(def.catchall, ctx, {
|
|
...params,
|
|
path: [...params.path, "additionalProperties"],
|
|
});
|
|
}
|
|
};
|
|
exports.objectProcessor = objectProcessor;
|
|
const unionProcessor = (schema, ctx, json, params) => {
|
|
const def = schema._zod.def;
|
|
// Exclusive unions (inclusive === false) use oneOf (exactly one match) instead of anyOf (one or more matches). This includes both z.xor() and discriminated unions
|
|
const isExclusive = def.inclusive === false;
|
|
const options = def.options.map((x, i) => (0, to_json_schema_js_1.processSchema)(x, ctx, {
|
|
...params,
|
|
path: [...params.path, isExclusive ? "oneOf" : "anyOf", i],
|
|
}));
|
|
if (isExclusive) {
|
|
json.oneOf = options;
|
|
}
|
|
else {
|
|
json.anyOf = options;
|
|
}
|
|
};
|
|
exports.unionProcessor = unionProcessor;
|
|
const intersectionProcessor = (schema, ctx, json, params) => {
|
|
const def = schema._zod.def;
|
|
const a = (0, to_json_schema_js_1.processSchema)(def.left, ctx, {
|
|
...params,
|
|
path: [...params.path, "allOf", 0],
|
|
});
|
|
const b = (0, to_json_schema_js_1.processSchema)(def.right, ctx, {
|
|
...params,
|
|
path: [...params.path, "allOf", 1],
|
|
});
|
|
const isSimpleIntersection = (val) => "allOf" in val && Object.keys(val).length === 1;
|
|
const allOf = [
|
|
...(isSimpleIntersection(a) ? a.allOf : [a]),
|
|
...(isSimpleIntersection(b) ? b.allOf : [b]),
|
|
];
|
|
json.allOf = allOf;
|
|
// Recorded innermost first, so a nested intersection has already folded by the time this one is considered. The array is the handle rather than the schema, because a wrapper that inherits this schema shares the same array; `finalize` folds every object holding it. See `foldIntersection`.
|
|
ctx.intersections.push(allOf);
|
|
};
|
|
exports.intersectionProcessor = intersectionProcessor;
|
|
const tupleProcessor = (schema, ctx, _json, params) => {
|
|
const json = _json;
|
|
const def = schema._zod.def;
|
|
json.type = "array";
|
|
const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
|
|
const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
|
|
const prefixItems = def.items.map((x, i) => (0, to_json_schema_js_1.processSchema)(x, ctx, {
|
|
...params,
|
|
path: [...params.path, prefixPath, i],
|
|
}));
|
|
const rest = def.rest
|
|
? (0, to_json_schema_js_1.processSchema)(def.rest, ctx, {
|
|
...params,
|
|
path: [...params.path, restPath, ...(ctx.target === "openapi-3.0" ? [def.items.length] : [])],
|
|
})
|
|
: null;
|
|
let minItems = def.items.length;
|
|
while (minItems > 0) {
|
|
const item = def.items[minItems - 1];
|
|
const optional = ctx.io === "input" ? inputOptin(item) !== undefined : item._zod.optout === "optional";
|
|
if (!optional)
|
|
break;
|
|
minItems--;
|
|
}
|
|
const maxItems = def.items.length;
|
|
const isClosed = !def.rest;
|
|
if (ctx.target === "draft-2020-12") {
|
|
json.prefixItems = prefixItems;
|
|
if (isClosed) {
|
|
json.items = false;
|
|
}
|
|
else if (rest) {
|
|
json.items = rest;
|
|
}
|
|
if (minItems > 0)
|
|
json.minItems = minItems;
|
|
if (isClosed)
|
|
json.maxItems = maxItems;
|
|
}
|
|
else if (ctx.target === "openapi-3.0") {
|
|
json.items = {
|
|
anyOf: prefixItems,
|
|
};
|
|
if (rest) {
|
|
json.items.anyOf.push(rest);
|
|
}
|
|
if (minItems > 0)
|
|
json.minItems = minItems;
|
|
if (isClosed)
|
|
json.maxItems = maxItems;
|
|
}
|
|
else {
|
|
json.items = prefixItems;
|
|
if (isClosed) {
|
|
json.additionalItems = false;
|
|
}
|
|
else if (rest) {
|
|
json.additionalItems = rest;
|
|
}
|
|
if (minItems > 0)
|
|
json.minItems = minItems;
|
|
if (isClosed)
|
|
json.maxItems = maxItems;
|
|
}
|
|
// explicit user-defined length checks take precedence
|
|
const { minimum, maximum } = aggregateChecks(schema);
|
|
if (typeof minimum === "number")
|
|
json.minItems = minimum;
|
|
if (typeof maximum === "number")
|
|
json.maxItems = maximum;
|
|
};
|
|
exports.tupleProcessor = tupleProcessor;
|
|
/** JSON object keys are always strings, so a numeric record key schema is re-expressed over the
|
|
* numeric-string form the record parser matches. Deferred to `finalize`, after the flatten: a key
|
|
* behind a wrapper only carries its own `type` before then, and a union key only has its branches.
|
|
*
|
|
* A numeric bound cannot apply to a property name, so `minimum` and its siblings are dropped rather
|
|
* than carried over: keeping them beside `type: "string"` reproduces the match-nothing schema this
|
|
* exists to fix. A key that carries one therefore emits wider than the record parses — `z.record(z.number().min(5), V)`
|
|
* accepts `"3"` — which is the deliberate trade, since throwing on it would reject an ordinary schema
|
|
* outright. */
|
|
function stringifyKeyNames(bySchema, json, visited) {
|
|
// an extracted key that rewrites cannot go on sharing its definition — the string form a key position needs is not the number form every other reference wants — so it inlines. One that does not rewrite keeps the `$ref`.
|
|
if (json.$ref) {
|
|
// a recursive key holds its own reference inside its definition, so a node already on the path is left alone rather than resolved again
|
|
if (visited.has(json))
|
|
return json;
|
|
visited.add(json);
|
|
const def = bySchema.get(json)?.def;
|
|
if (!def)
|
|
return json;
|
|
const inlined = stringifyKeyNames(bySchema, def, visited);
|
|
return inlined === def ? json : inlined;
|
|
}
|
|
for (const keyword of ["anyOf", "oneOf"]) {
|
|
const branches = json[keyword];
|
|
if (!Array.isArray(branches))
|
|
continue;
|
|
const mapped = branches.map((branch) => stringifyKeyNames(bySchema, branch, visited));
|
|
// rebuilding regardless would detach a key that had nothing to re-express, dropping its `$ref` and leaking the internal `id`
|
|
if (mapped.some((branch, i) => branch !== branches[i]))
|
|
json = { ...json, [keyword]: mapped };
|
|
}
|
|
// a member that already admits a string leaves the key unconstrained, so the node's own type re-expresses only when every member is numeric
|
|
const types = Array.isArray(json.type) ? json.type : [json.type];
|
|
const numericType = !types.includes("string") && types.some((t) => t === "number" || t === "integer");
|
|
// a heterogeneous key carries no type at all, so its numeric members are caught here instead
|
|
const values = json.enum ?? (json.const !== undefined ? [json.const] : undefined);
|
|
if (!numericType && !values?.some((v) => typeof v === "number"))
|
|
return json;
|
|
const { minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, format, id, ...rest } = json;
|
|
if (rest.enum)
|
|
rest.enum = rest.enum.map((v) => (typeof v === "number" ? String(v) : v));
|
|
else if (typeof rest.const === "number")
|
|
rest.const = String(rest.const);
|
|
// a heterogeneous key keeps its absent type: the stringified members already say what a key may be
|
|
if (!numericType)
|
|
return rest;
|
|
rest.type = "string";
|
|
if (!values)
|
|
rest.pattern = (types.includes("number") ? regexes.number : regexes.integer).source;
|
|
return rest;
|
|
}
|
|
/** Every record of one conversion, so the carriers are found in a single pass rather than once per record. */
|
|
const pendingRecords = new WeakMap();
|
|
function rewriteKeyNames(ctx) {
|
|
// an extracted key is resolved by the object `extractToDef` left in its place, so the map is built once rather than searched per reference. `_zod.toJSONSchema` can hand the same object to two schemas, so the first entry carrying a body wins, as a search would have found it.
|
|
const bySchema = new Map();
|
|
for (const entry of ctx.seen.values()) {
|
|
if (entry.def && !bySchema.has(entry.schema))
|
|
bySchema.set(entry.schema, entry);
|
|
}
|
|
const rewrites = new Map();
|
|
for (const record of pendingRecords.get(ctx) ?? []) {
|
|
const seen = ctx.seen.get(record);
|
|
const names = (seen?.def ?? seen?.schema)?.propertyNames;
|
|
if (!names || names === true || rewrites.has(names))
|
|
continue;
|
|
const rewritten = stringifyKeyNames(bySchema, names, new Set());
|
|
if (rewritten !== names)
|
|
rewrites.set(names, rewritten);
|
|
}
|
|
if (!rewrites.size)
|
|
return;
|
|
// the flatten has already copied each record's own properties onto every wrapper by reference, and an extracted body is another such copy, so every carrier holding a rewritten key is updated together
|
|
for (const entry of ctx.seen.values()) {
|
|
for (const carrier of [entry.schema, entry.def]) {
|
|
const rewritten = carrier && rewrites.get(carrier.propertyNames);
|
|
if (rewritten)
|
|
carrier.propertyNames = rewritten;
|
|
}
|
|
}
|
|
}
|
|
const recordProcessor = (schema, ctx, _json, params) => {
|
|
const json = _json;
|
|
const def = schema._zod.def;
|
|
json.type = "object";
|
|
// For looseRecord with regex patterns, use patternProperties. This correctly represents "only validate keys matching the pattern" semantics and composes well with allOf (intersections)
|
|
const keyType = def.keyType;
|
|
const patterns = aggregateChecks(keyType).patterns;
|
|
if (def.mode === "loose" && patterns && patterns.size > 0) {
|
|
// Use patternProperties for looseRecord with regex patterns
|
|
const valueSchema = (0, to_json_schema_js_1.processSchema)(def.valueType, ctx, {
|
|
...params,
|
|
path: [...params.path, "patternProperties", "*"],
|
|
});
|
|
json.patternProperties = {};
|
|
for (const pattern of patterns) {
|
|
(0, util_js_1.assignProp)(json.patternProperties, exactPattern(pattern).source, valueSchema);
|
|
}
|
|
}
|
|
else {
|
|
// Default behavior: use propertyNames + additionalProperties
|
|
if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
|
|
json.propertyNames = (0, to_json_schema_js_1.processSchema)(def.keyType, ctx, {
|
|
...params,
|
|
path: [...params.path, "propertyNames"],
|
|
});
|
|
let pending = pendingRecords.get(ctx);
|
|
if (!pending) {
|
|
pending = [];
|
|
pendingRecords.set(ctx, pending);
|
|
ctx.deferred.push(() => rewriteKeyNames(ctx));
|
|
}
|
|
pending.push(schema);
|
|
}
|
|
json.additionalProperties = (0, to_json_schema_js_1.processSchema)(def.valueType, ctx, {
|
|
...params,
|
|
path: [...params.path, "additionalProperties"],
|
|
});
|
|
}
|
|
// Add required for keys with discrete values (enum, literal, etc.)
|
|
const keyValues = keyType._zod.values;
|
|
// Every key shares one value schema, so an optional-in value makes the whole key set omittable on input. Output keeps them: the exhaustive branch assigns every key, even one whose value came back undefined.
|
|
const omittableOnInput = ctx.io === "input" && inputOptin(def.valueType) !== undefined;
|
|
if (keyValues && !def.partial && !omittableOnInput) {
|
|
const validKeyValues = [...keyValues].filter((v) => typeof v === "string" || typeof v === "number");
|
|
if (validKeyValues.length > 0) {
|
|
json.required = validKeyValues.map(String);
|
|
}
|
|
}
|
|
};
|
|
exports.recordProcessor = recordProcessor;
|
|
const nullableProcessor = (schema, ctx, json, params) => {
|
|
const def = schema._zod.def;
|
|
const inner = (0, to_json_schema_js_1.processSchema)(def.innerType, ctx, params);
|
|
const seen = ctx.seen.get(schema);
|
|
if (ctx.target === "openapi-3.0") {
|
|
seen.ref = def.innerType;
|
|
json.nullable = true;
|
|
}
|
|
else {
|
|
json.anyOf = [inner, { type: "null" }];
|
|
}
|
|
};
|
|
exports.nullableProcessor = nullableProcessor;
|
|
const nonoptionalProcessor = (schema, ctx, _json, params) => {
|
|
const def = schema._zod.def;
|
|
(0, to_json_schema_js_1.processSchema)(def.innerType, ctx, params);
|
|
const seen = ctx.seen.get(schema);
|
|
seen.ref = def.innerType;
|
|
};
|
|
exports.nonoptionalProcessor = nonoptionalProcessor;
|
|
/** Round-trips a default value through JSON so the emitted schema is guaranteed to be valid JSON.
|
|
* A BigInt has no reliable encoding, so it goes through `unrepresentable` like any other
|
|
* unrepresentable value. Returns a sentinel when the caller must not write a default of its own. */
|
|
const UNREPRESENTABLE_DEFAULT = Symbol();
|
|
function serializeDefaultValue(value, schema, ctx, json, params) {
|
|
let unrepresentable = false;
|
|
const serialized = JSON.stringify(value, (_, val) => {
|
|
if (typeof val !== "bigint")
|
|
return val;
|
|
unrepresentable = true;
|
|
return null;
|
|
});
|
|
if (!unrepresentable)
|
|
return JSON.parse(serialized);
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "BigInt defaults cannot be represented in JSON Schema");
|
|
return UNREPRESENTABLE_DEFAULT;
|
|
}
|
|
const defaultProcessor = (schema, ctx, json, params) => {
|
|
const def = schema._zod.def;
|
|
(0, to_json_schema_js_1.processSchema)(def.innerType, ctx, params);
|
|
const seen = ctx.seen.get(schema);
|
|
seen.ref = def.innerType;
|
|
const value = serializeDefaultValue(def.defaultValue, schema, ctx, json, params);
|
|
if (value !== UNREPRESENTABLE_DEFAULT)
|
|
json.default = value;
|
|
};
|
|
exports.defaultProcessor = defaultProcessor;
|
|
const prefaultProcessor = (schema, ctx, json, params) => {
|
|
const def = schema._zod.def;
|
|
(0, to_json_schema_js_1.processSchema)(def.innerType, ctx, params);
|
|
const seen = ctx.seen.get(schema);
|
|
seen.ref = def.innerType;
|
|
if (ctx.io !== "input")
|
|
return;
|
|
const value = serializeDefaultValue(def.defaultValue, schema, ctx, json, params);
|
|
if (value !== UNREPRESENTABLE_DEFAULT)
|
|
json._prefault = value;
|
|
};
|
|
exports.prefaultProcessor = prefaultProcessor;
|
|
const catchProcessor = (schema, ctx, json, params) => {
|
|
const def = schema._zod.def;
|
|
(0, to_json_schema_js_1.processSchema)(def.innerType, ctx, params);
|
|
const seen = ctx.seen.get(schema);
|
|
seen.ref = def.innerType;
|
|
let catchValue;
|
|
try {
|
|
catchValue = def.catchValue(undefined);
|
|
}
|
|
catch {
|
|
(0, to_json_schema_js_1.handleUnrepresentable)(schema, ctx, json, params, "Dynamic catch values are not supported in JSON Schema");
|
|
return;
|
|
}
|
|
json.default = catchValue;
|
|
};
|
|
exports.catchProcessor = catchProcessor;
|
|
const pipeProcessor = (schema, ctx, _json, params) => {
|
|
const def = schema._zod.def;
|
|
const inIsTransform = def.in._zod.traits.has("$ZodTransform");
|
|
const innerType = ctx.io === "input" ? (inIsTransform ? def.out : def.in) : def.out;
|
|
(0, to_json_schema_js_1.processSchema)(innerType, ctx, params);
|
|
const seen = ctx.seen.get(schema);
|
|
seen.ref = innerType;
|
|
};
|
|
exports.pipeProcessor = pipeProcessor;
|
|
const readonlyProcessor = (schema, ctx, json, params) => {
|
|
const def = schema._zod.def;
|
|
(0, to_json_schema_js_1.processSchema)(def.innerType, ctx, params);
|
|
const seen = ctx.seen.get(schema);
|
|
seen.ref = def.innerType;
|
|
json.readOnly = true;
|
|
};
|
|
exports.readonlyProcessor = readonlyProcessor;
|
|
const promiseProcessor = (schema, ctx, _json, params) => {
|
|
const def = schema._zod.def;
|
|
(0, to_json_schema_js_1.processSchema)(def.innerType, ctx, params);
|
|
const seen = ctx.seen.get(schema);
|
|
seen.ref = def.innerType;
|
|
};
|
|
exports.promiseProcessor = promiseProcessor;
|
|
const optionalProcessor = (schema, ctx, _json, params) => {
|
|
const def = schema._zod.def;
|
|
(0, to_json_schema_js_1.processSchema)(def.innerType, ctx, params);
|
|
const seen = ctx.seen.get(schema);
|
|
seen.ref = def.innerType;
|
|
};
|
|
exports.optionalProcessor = optionalProcessor;
|
|
const lazyProcessor = (schema, ctx, _json, params) => {
|
|
const innerType = schema._zod.innerType;
|
|
(0, to_json_schema_js_1.processSchema)(innerType, ctx, params);
|
|
const seen = ctx.seen.get(schema);
|
|
seen.ref = innerType;
|
|
};
|
|
exports.lazyProcessor = lazyProcessor;
|
|
// ==================== ALL PROCESSORS ====================
|
|
exports.allProcessors = {
|
|
string: exports.stringProcessor,
|
|
number: exports.numberProcessor,
|
|
boolean: exports.booleanProcessor,
|
|
bigint: exports.bigintProcessor,
|
|
symbol: exports.symbolProcessor,
|
|
null: exports.nullProcessor,
|
|
undefined: exports.undefinedProcessor,
|
|
void: exports.voidProcessor,
|
|
never: exports.neverProcessor,
|
|
any: exports.anyProcessor,
|
|
unknown: exports.unknownProcessor,
|
|
date: exports.dateProcessor,
|
|
enum: exports.enumProcessor,
|
|
literal: exports.literalProcessor,
|
|
nan: exports.nanProcessor,
|
|
template_literal: exports.templateLiteralProcessor,
|
|
file: exports.fileProcessor,
|
|
success: exports.successProcessor,
|
|
custom: exports.customProcessor,
|
|
function: exports.functionProcessor,
|
|
transform: exports.transformProcessor,
|
|
map: exports.mapProcessor,
|
|
set: exports.setProcessor,
|
|
array: exports.arrayProcessor,
|
|
object: exports.objectProcessor,
|
|
union: exports.unionProcessor,
|
|
intersection: exports.intersectionProcessor,
|
|
tuple: exports.tupleProcessor,
|
|
record: exports.recordProcessor,
|
|
nullable: exports.nullableProcessor,
|
|
nonoptional: exports.nonoptionalProcessor,
|
|
default: exports.defaultProcessor,
|
|
prefault: exports.prefaultProcessor,
|
|
catch: exports.catchProcessor,
|
|
pipe: exports.pipeProcessor,
|
|
readonly: exports.readonlyProcessor,
|
|
promise: exports.promiseProcessor,
|
|
optional: exports.optionalProcessor,
|
|
lazy: exports.lazyProcessor,
|
|
};
|
|
function toJSONSchema(input, params) {
|
|
if ("_idmap" in input) {
|
|
// Registry case
|
|
const registry = input;
|
|
const ctx = (0, to_json_schema_js_1.initializeContext)({ ...params, processors: exports.allProcessors });
|
|
const defs = {};
|
|
// First pass: process all schemas to build the seen map
|
|
for (const entry of registry._idmap.entries()) {
|
|
const [_, schema] = entry;
|
|
(0, to_json_schema_js_1.processSchema)(schema, ctx);
|
|
}
|
|
const schemas = {};
|
|
const external = {
|
|
registry,
|
|
uri: params?.uri,
|
|
defs,
|
|
};
|
|
// Update the context with external configuration
|
|
ctx.external = external;
|
|
// Second pass: emit each schema
|
|
for (const entry of registry._idmap.entries()) {
|
|
const [key, schema] = entry;
|
|
(0, to_json_schema_js_1.extractDefs)(ctx, schema);
|
|
(0, util_js_1.assignProp)(schemas, key, (0, to_json_schema_js_1.finalize)(ctx, schema));
|
|
}
|
|
if (Object.keys(defs).length > 0) {
|
|
const defsSegment = ctx.target === "draft-2020-12" ? "$defs" : "definitions";
|
|
schemas.__shared = {
|
|
[defsSegment]: defs,
|
|
};
|
|
}
|
|
return { schemas };
|
|
}
|
|
// Single schema case
|
|
const ctx = (0, to_json_schema_js_1.initializeContext)({ ...params, processors: exports.allProcessors });
|
|
(0, to_json_schema_js_1.processSchema)(input, ctx);
|
|
(0, to_json_schema_js_1.extractDefs)(ctx, input);
|
|
return (0, to_json_schema_js_1.finalize)(ctx, input);
|
|
}
|
|
|
|
// seal-cjs-exports
|
|
(function () {
|
|
var keys = Object.getOwnPropertyNames(exports);
|
|
for (var i = 0; i < keys.length; i++) {
|
|
var desc = Object.getOwnPropertyDescriptor(exports, keys[i]);
|
|
if (!desc || !desc.get || !desc.configurable) continue;
|
|
var value;
|
|
try {
|
|
value = desc.get();
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
// a circular require may not have settled this one yet, so leave it live
|
|
if (value === undefined) continue;
|
|
Object.defineProperty(exports, keys[i], { value: value, writable: false, enumerable: desc.enumerable, configurable: false });
|
|
}
|
|
Object.freeze(exports);
|
|
})();
|