UNPKG

zod

Version:

TypeScript-first schema declaration and validation library with static type inference

618 lines (617 loc) • 23 kB
"use strict"; 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); }