zod
Version:
TypeScript-first schema declaration and validation library with static type inference
618 lines (617 loc) • 23 kB
JavaScript
;
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.toJSONSchema = toJSONSchema;
const to_json_schema_js_1 = require("./to-json-schema.cjs");
const util_js_1 = require("./util.cjs");
const formatMap = {
guid: "uuid",
url: "uri",
datetime: "date-time",
json_string: "json-string",
regex: "", // do not set
};
// ==================== SIMPLE TYPE PROCESSORS ====================
const stringProcessor = (schema, ctx, _json, _params) => {
const json = _json;
json.type = "string";
const { minimum, maximum, format, patterns, contentEncoding } = schema._zod
.bag;
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
}
if (contentEncoding)
json.contentEncoding = contentEncoding;
if (patterns && patterns.size > 0) {
const regexes = [...patterns];
if (regexes.length === 1)
json.pattern = regexes[0].source;
else if (regexes.length > 1) {
json.allOf = [
...regexes.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, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag;
if (typeof format === "string" && format.includes("int"))
json.type = "integer";
else
json.type = "number";
if (typeof exclusiveMinimum === "number") {
if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") {
json.minimum = exclusiveMinimum;
json.exclusiveMinimum = true;
}
else {
json.exclusiveMinimum = exclusiveMinimum;
}
}
if (typeof minimum === "number") {
json.minimum = minimum;
if (typeof exclusiveMinimum === "number" && ctx.target !== "draft-04") {
if (exclusiveMinimum >= minimum)
delete json.minimum;
else
delete json.exclusiveMinimum;
}
}
if (typeof exclusiveMaximum === "number") {
if (ctx.target === "draft-04" || ctx.target === "openapi-3.0") {
json.maximum = exclusiveMaximum;
json.exclusiveMaximum = true;
}
else {
json.exclusiveMaximum = exclusiveMaximum;
}
}
if (typeof maximum === "number") {
json.maximum = maximum;
if (typeof exclusiveMaximum === "number" && ctx.target !== "draft-04") {
if (exclusiveMaximum <= maximum)
delete json.maximum;
else
delete json.exclusiveMaximum;
}
}
if (typeof multipleOf === "number")
json.multipleOf = multipleOf;
};
exports.numberProcessor = numberProcessor;
const booleanProcessor = (_schema, _ctx, json, _params) => {
json.type = "boolean";
};
exports.booleanProcessor = booleanProcessor;
const bigintProcessor = (_schema, ctx, _json, _params) => {
if (ctx.unrepresentable === "throw") {
throw new Error("BigInt cannot be represented in JSON Schema");
}
};
exports.bigintProcessor = bigintProcessor;
const symbolProcessor = (_schema, ctx, _json, _params) => {
if (ctx.unrepresentable === "throw") {
throw new Error("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) => {
if (ctx.unrepresentable === "throw") {
throw new Error("Undefined cannot be represented in JSON Schema");
}
};
exports.undefinedProcessor = undefinedProcessor;
const voidProcessor = (_schema, ctx, _json, _params) => {
if (ctx.unrepresentable === "throw") {
throw new Error("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) => {
if (ctx.unrepresentable === "throw") {
throw new Error("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);
// 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;
const vals = [];
for (const val of def.values) {
if (val === undefined) {
if (ctx.unrepresentable === "throw") {
throw new Error("Literal `undefined` cannot be represented in JSON Schema");
}
else {
// do not add to vals
}
}
else if (typeof val === "bigint") {
if (ctx.unrepresentable === "throw") {
throw new Error("BigInt literals cannot be represented in JSON Schema");
}
else {
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) => {
if (ctx.unrepresentable === "throw") {
throw new Error("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;
const file = {
type: "string",
format: "binary",
contentEncoding: "binary",
};
const { minimum, maximum, mime } = schema._zod.bag;
if (minimum !== undefined)
file.minLength = minimum;
if (maximum !== undefined)
file.maxLength = maximum;
if (mime) {
if (mime.length === 1) {
file.contentMediaType = mime[0];
Object.assign(_json, file);
}
else {
_json.anyOf = mime.map((m) => {
const mFile = { ...file, contentMediaType: m };
return mFile;
});
}
}
else {
Object.assign(_json, file);
}
};
exports.fileProcessor = fileProcessor;
const successProcessor = (_schema, _ctx, json, _params) => {
json.type = "boolean";
};
exports.successProcessor = successProcessor;
const customProcessor = (_schema, ctx, _json, _params) => {
if (ctx.unrepresentable === "throw") {
throw new Error("Custom types cannot be represented in JSON Schema");
}
};
exports.customProcessor = customProcessor;
const functionProcessor = (_schema, ctx, _json, _params) => {
if (ctx.unrepresentable === "throw") {
throw new Error("Function types cannot be represented in JSON Schema");
}
};
exports.functionProcessor = functionProcessor;
const transformProcessor = (_schema, ctx, _json, _params) => {
if (ctx.unrepresentable === "throw") {
throw new Error("Transforms cannot be represented in JSON Schema");
}
};
exports.transformProcessor = transformProcessor;
const mapProcessor = (_schema, ctx, _json, _params) => {
if (ctx.unrepresentable === "throw") {
throw new Error("Map cannot be represented in JSON Schema");
}
};
exports.mapProcessor = mapProcessor;
const setProcessor = (_schema, ctx, _json, _params) => {
if (ctx.unrepresentable === "throw") {
throw new Error("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 } = schema._zod.bag;
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.process)(def.element, ctx, { ...params, path: [...params.path, "items"] });
};
exports.arrayProcessor = arrayProcessor;
const objectProcessor = (schema, ctx, _json, params) => {
const json = _json;
const def = schema._zod.def;
json.type = "object";
json.properties = {};
const shape = def.shape;
for (const key in shape) {
json.properties[key] = (0, to_json_schema_js_1.process)(shape[key], ctx, {
...params,
path: [...params.path, "properties", key],
});
}
// required keys
const allKeys = new Set(Object.keys(shape));
const requiredKeys = new Set([...allKeys].filter((key) => {
const v = def.shape[key]._zod;
if (ctx.io === "input") {
return v.optin === undefined;
}
else {
return v.optout === undefined;
}
}));
if (requiredKeys.size > 0) {
json.required = Array.from(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.process)(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.process)(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.process)(def.left, ctx, {
...params,
path: [...params.path, "allOf", 0],
});
const b = (0, to_json_schema_js_1.process)(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;
};
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.process)(x, ctx, {
...params,
path: [...params.path, prefixPath, i],
}));
const rest = def.rest
? (0, to_json_schema_js_1.process)(def.rest, ctx, {
...params,
path: [...params.path, restPath, ...(ctx.target === "openapi-3.0" ? [def.items.length] : [])],
})
: null;
if (ctx.target === "draft-2020-12") {
json.prefixItems = prefixItems;
if (rest) {
json.items = rest;
}
}
else if (ctx.target === "openapi-3.0") {
json.items = {
anyOf: prefixItems,
};
if (rest) {
json.items.anyOf.push(rest);
}
json.minItems = prefixItems.length;
if (!rest) {
json.maxItems = prefixItems.length;
}
}
else {
json.items = prefixItems;
if (rest) {
json.additionalItems = rest;
}
}
// length
const { minimum, maximum } = schema._zod.bag;
if (typeof minimum === "number")
json.minItems = minimum;
if (typeof maximum === "number")
json.maxItems = maximum;
};
exports.tupleProcessor = tupleProcessor;
const recordProcessor = (schema, ctx, _json, params) => {
const json = _json;
const def = schema._zod.def;
json.type = "object";
if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
json.propertyNames = (0, to_json_schema_js_1.process)(def.keyType, ctx, {
...params,
path: [...params.path, "propertyNames"],
});
}
json.additionalProperties = (0, to_json_schema_js_1.process)(def.valueType, ctx, {
...params,
path: [...params.path, "additionalProperties"],
});
};
exports.recordProcessor = recordProcessor;
const nullableProcessor = (schema, ctx, json, params) => {
const def = schema._zod.def;
const inner = (0, to_json_schema_js_1.process)(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.process)(def.innerType, ctx, params);
const seen = ctx.seen.get(schema);
seen.ref = def.innerType;
};
exports.nonoptionalProcessor = nonoptionalProcessor;
const defaultProcessor = (schema, ctx, json, params) => {
const def = schema._zod.def;
(0, to_json_schema_js_1.process)(def.innerType, ctx, params);
const seen = ctx.seen.get(schema);
seen.ref = def.innerType;
json.default = JSON.parse(JSON.stringify(def.defaultValue));
};
exports.defaultProcessor = defaultProcessor;
const prefaultProcessor = (schema, ctx, json, params) => {
const def = schema._zod.def;
(0, to_json_schema_js_1.process)(def.innerType, ctx, params);
const seen = ctx.seen.get(schema);
seen.ref = def.innerType;
if (ctx.io === "input")
json._prefault = JSON.parse(JSON.stringify(def.defaultValue));
};
exports.prefaultProcessor = prefaultProcessor;
const catchProcessor = (schema, ctx, json, params) => {
const def = schema._zod.def;
(0, to_json_schema_js_1.process)(def.innerType, ctx, params);
const seen = ctx.seen.get(schema);
seen.ref = def.innerType;
let catchValue;
try {
catchValue = def.catchValue(undefined);
}
catch {
throw new Error("Dynamic catch values are not supported in JSON Schema");
}
json.default = catchValue;
};
exports.catchProcessor = catchProcessor;
const pipeProcessor = (schema, ctx, _json, params) => {
const def = schema._zod.def;
const innerType = ctx.io === "input" ? (def.in._zod.def.type === "transform" ? def.out : def.in) : def.out;
(0, to_json_schema_js_1.process)(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.process)(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.process)(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.process)(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.process)(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.process)(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);
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.process)(input, ctx);
(0, to_json_schema_js_1.extractDefs)(ctx, input);
return (0, to_json_schema_js_1.finalize)(ctx, input);
}