to-json-schema.cjs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.createStandardJSONSchemaMethod = exports.createToJSONSchemaMethod = void 0;
  4. exports.initializeContext = initializeContext;
  5. exports.process = process;
  6. exports.extractDefs = extractDefs;
  7. exports.finalize = finalize;
  8. const registries_js_1 = require("./registries.cjs");
  9. // function initializeContext<T extends schemas.$ZodType>(inputs: JSONSchemaGeneratorParams<T>): ToJSONSchemaContext<T> {
  10. // return {
  11. // processor: inputs.processor,
  12. // metadataRegistry: inputs.metadata ?? globalRegistry,
  13. // target: inputs.target ?? "draft-2020-12",
  14. // unrepresentable: inputs.unrepresentable ?? "throw",
  15. // };
  16. // }
  17. function initializeContext(params) {
  18. // Normalize target: convert old non-hyphenated versions to hyphenated versions
  19. let target = params?.target ?? "draft-2020-12";
  20. if (target === "draft-4")
  21. target = "draft-04";
  22. if (target === "draft-7")
  23. target = "draft-07";
  24. return {
  25. processors: params.processors ?? {},
  26. metadataRegistry: params?.metadata ?? registries_js_1.globalRegistry,
  27. target,
  28. unrepresentable: params?.unrepresentable ?? "throw",
  29. override: params?.override ?? (() => { }),
  30. io: params?.io ?? "output",
  31. counter: 0,
  32. seen: new Map(),
  33. cycles: params?.cycles ?? "ref",
  34. reused: params?.reused ?? "inline",
  35. external: params?.external ?? undefined,
  36. };
  37. }
  38. function process(schema, ctx, _params = { path: [], schemaPath: [] }) {
  39. var _a;
  40. const def = schema._zod.def;
  41. // check for schema in seens
  42. const seen = ctx.seen.get(schema);
  43. if (seen) {
  44. seen.count++;
  45. // check if cycle
  46. const isCycle = _params.schemaPath.includes(schema);
  47. if (isCycle) {
  48. seen.cycle = _params.path;
  49. }
  50. return seen.schema;
  51. }
  52. // initialize
  53. const result = { schema: {}, count: 1, cycle: undefined, path: _params.path };
  54. ctx.seen.set(schema, result);
  55. // custom method overrides default behavior
  56. const overrideSchema = schema._zod.toJSONSchema?.();
  57. if (overrideSchema) {
  58. result.schema = overrideSchema;
  59. }
  60. else {
  61. const params = {
  62. ..._params,
  63. schemaPath: [..._params.schemaPath, schema],
  64. path: _params.path,
  65. };
  66. if (schema._zod.processJSONSchema) {
  67. schema._zod.processJSONSchema(ctx, result.schema, params);
  68. }
  69. else {
  70. const _json = result.schema;
  71. const processor = ctx.processors[def.type];
  72. if (!processor) {
  73. throw new Error(`[toJSONSchema]: Non-representable type encountered: ${def.type}`);
  74. }
  75. processor(schema, ctx, _json, params);
  76. }
  77. const parent = schema._zod.parent;
  78. if (parent) {
  79. // Also set ref if processor didn't (for inheritance)
  80. if (!result.ref)
  81. result.ref = parent;
  82. process(parent, ctx, params);
  83. ctx.seen.get(parent).isParent = true;
  84. }
  85. }
  86. // metadata
  87. const meta = ctx.metadataRegistry.get(schema);
  88. if (meta)
  89. Object.assign(result.schema, meta);
  90. if (ctx.io === "input" && isTransforming(schema)) {
  91. // examples/defaults only apply to output type of pipe
  92. delete result.schema.examples;
  93. delete result.schema.default;
  94. }
  95. // set prefault as default
  96. if (ctx.io === "input" && result.schema._prefault)
  97. (_a = result.schema).default ?? (_a.default = result.schema._prefault);
  98. delete result.schema._prefault;
  99. // pulling fresh from ctx.seen in case it was overwritten
  100. const _result = ctx.seen.get(schema);
  101. return _result.schema;
  102. }
  103. function extractDefs(ctx, schema
  104. // params: EmitParams
  105. ) {
  106. // iterate over seen map;
  107. const root = ctx.seen.get(schema);
  108. if (!root)
  109. throw new Error("Unprocessed schema. This is a bug in Zod.");
  110. // Track ids to detect duplicates across different schemas
  111. const idToSchema = new Map();
  112. for (const entry of ctx.seen.entries()) {
  113. const id = ctx.metadataRegistry.get(entry[0])?.id;
  114. if (id) {
  115. const existing = idToSchema.get(id);
  116. if (existing && existing !== entry[0]) {
  117. throw new Error(`Duplicate schema id "${id}" detected during JSON Schema conversion. Two different schemas cannot share the same id when converted together.`);
  118. }
  119. idToSchema.set(id, entry[0]);
  120. }
  121. }
  122. // returns a ref to the schema
  123. // defId will be empty if the ref points to an external schema (or #)
  124. const makeURI = (entry) => {
  125. // comparing the seen objects because sometimes
  126. // multiple schemas map to the same seen object.
  127. // e.g. lazy
  128. // external is configured
  129. const defsSegment = ctx.target === "draft-2020-12" ? "$defs" : "definitions";
  130. if (ctx.external) {
  131. const externalId = ctx.external.registry.get(entry[0])?.id; // ?? "__shared";// `__schema${ctx.counter++}`;
  132. // check if schema is in the external registry
  133. const uriGenerator = ctx.external.uri ?? ((id) => id);
  134. if (externalId) {
  135. return { ref: uriGenerator(externalId) };
  136. }
  137. // otherwise, add to __shared
  138. const id = entry[1].defId ?? entry[1].schema.id ?? `schema${ctx.counter++}`;
  139. entry[1].defId = id; // set defId so it will be reused if needed
  140. return { defId: id, ref: `${uriGenerator("__shared")}#/${defsSegment}/${id}` };
  141. }
  142. if (entry[1] === root) {
  143. return { ref: "#" };
  144. }
  145. // self-contained schema
  146. const uriPrefix = `#`;
  147. const defUriPrefix = `${uriPrefix}/${defsSegment}/`;
  148. const defId = entry[1].schema.id ?? `__schema${ctx.counter++}`;
  149. return { defId, ref: defUriPrefix + defId };
  150. };
  151. // stored cached version in `def` property
  152. // remove all properties, set $ref
  153. const extractToDef = (entry) => {
  154. // if the schema is already a reference, do not extract it
  155. if (entry[1].schema.$ref) {
  156. return;
  157. }
  158. const seen = entry[1];
  159. const { ref, defId } = makeURI(entry);
  160. seen.def = { ...seen.schema };
  161. // defId won't be set if the schema is a reference to an external schema
  162. // or if the schema is the root schema
  163. if (defId)
  164. seen.defId = defId;
  165. // wipe away all properties except $ref
  166. const schema = seen.schema;
  167. for (const key in schema) {
  168. delete schema[key];
  169. }
  170. schema.$ref = ref;
  171. };
  172. // throw on cycles
  173. // break cycles
  174. if (ctx.cycles === "throw") {
  175. for (const entry of ctx.seen.entries()) {
  176. const seen = entry[1];
  177. if (seen.cycle) {
  178. throw new Error("Cycle detected: " +
  179. `#/${seen.cycle?.join("/")}/<root>` +
  180. '\n\nSet the `cycles` parameter to `"ref"` to resolve cyclical schemas with defs.');
  181. }
  182. }
  183. }
  184. // extract schemas into $defs
  185. for (const entry of ctx.seen.entries()) {
  186. const seen = entry[1];
  187. // convert root schema to # $ref
  188. if (schema === entry[0]) {
  189. extractToDef(entry); // this has special handling for the root schema
  190. continue;
  191. }
  192. // extract schemas that are in the external registry
  193. if (ctx.external) {
  194. const ext = ctx.external.registry.get(entry[0])?.id;
  195. if (schema !== entry[0] && ext) {
  196. extractToDef(entry);
  197. continue;
  198. }
  199. }
  200. // extract schemas with `id` meta
  201. const id = ctx.metadataRegistry.get(entry[0])?.id;
  202. if (id) {
  203. extractToDef(entry);
  204. continue;
  205. }
  206. // break cycles
  207. if (seen.cycle) {
  208. // any
  209. extractToDef(entry);
  210. continue;
  211. }
  212. // extract reused schemas
  213. if (seen.count > 1) {
  214. if (ctx.reused === "ref") {
  215. extractToDef(entry);
  216. // biome-ignore lint:
  217. continue;
  218. }
  219. }
  220. }
  221. }
  222. function finalize(ctx, schema) {
  223. const root = ctx.seen.get(schema);
  224. if (!root)
  225. throw new Error("Unprocessed schema. This is a bug in Zod.");
  226. // flatten refs - inherit properties from parent schemas
  227. const flattenRef = (zodSchema) => {
  228. const seen = ctx.seen.get(zodSchema);
  229. // already processed
  230. if (seen.ref === null)
  231. return;
  232. const schema = seen.def ?? seen.schema;
  233. const _cached = { ...schema };
  234. const ref = seen.ref;
  235. seen.ref = null; // prevent infinite recursion
  236. if (ref) {
  237. flattenRef(ref);
  238. const refSeen = ctx.seen.get(ref);
  239. const refSchema = refSeen.schema;
  240. // merge referenced schema into current
  241. if (refSchema.$ref && (ctx.target === "draft-07" || ctx.target === "draft-04" || ctx.target === "openapi-3.0")) {
  242. // older drafts can't combine $ref with other properties
  243. schema.allOf = schema.allOf ?? [];
  244. schema.allOf.push(refSchema);
  245. }
  246. else {
  247. Object.assign(schema, refSchema);
  248. }
  249. // restore child's own properties (child wins)
  250. Object.assign(schema, _cached);
  251. const isParentRef = zodSchema._zod.parent === ref;
  252. // For parent chain, child is a refinement - remove parent-only properties
  253. if (isParentRef) {
  254. for (const key in schema) {
  255. if (key === "$ref" || key === "allOf")
  256. continue;
  257. if (!(key in _cached)) {
  258. delete schema[key];
  259. }
  260. }
  261. }
  262. // When ref was extracted to $defs, remove properties that match the definition
  263. if (refSchema.$ref && refSeen.def) {
  264. for (const key in schema) {
  265. if (key === "$ref" || key === "allOf")
  266. continue;
  267. if (key in refSeen.def && JSON.stringify(schema[key]) === JSON.stringify(refSeen.def[key])) {
  268. delete schema[key];
  269. }
  270. }
  271. }
  272. }
  273. // If parent was extracted (has $ref), propagate $ref to this schema
  274. // This handles cases like: readonly().meta({id}).describe()
  275. // where processor sets ref to innerType but parent should be referenced
  276. const parent = zodSchema._zod.parent;
  277. if (parent && parent !== ref) {
  278. // Ensure parent is processed first so its def has inherited properties
  279. flattenRef(parent);
  280. const parentSeen = ctx.seen.get(parent);
  281. if (parentSeen?.schema.$ref) {
  282. schema.$ref = parentSeen.schema.$ref;
  283. // De-duplicate with parent's definition
  284. if (parentSeen.def) {
  285. for (const key in schema) {
  286. if (key === "$ref" || key === "allOf")
  287. continue;
  288. if (key in parentSeen.def && JSON.stringify(schema[key]) === JSON.stringify(parentSeen.def[key])) {
  289. delete schema[key];
  290. }
  291. }
  292. }
  293. }
  294. }
  295. // execute overrides
  296. ctx.override({
  297. zodSchema: zodSchema,
  298. jsonSchema: schema,
  299. path: seen.path ?? [],
  300. });
  301. };
  302. for (const entry of [...ctx.seen.entries()].reverse()) {
  303. flattenRef(entry[0]);
  304. }
  305. const result = {};
  306. if (ctx.target === "draft-2020-12") {
  307. result.$schema = "https://json-schema.org/draft/2020-12/schema";
  308. }
  309. else if (ctx.target === "draft-07") {
  310. result.$schema = "http://json-schema.org/draft-07/schema#";
  311. }
  312. else if (ctx.target === "draft-04") {
  313. result.$schema = "http://json-schema.org/draft-04/schema#";
  314. }
  315. else if (ctx.target === "openapi-3.0") {
  316. // OpenAPI 3.0 schema objects should not include a $schema property
  317. }
  318. else {
  319. // Arbitrary string values are allowed but won't have a $schema property set
  320. }
  321. if (ctx.external?.uri) {
  322. const id = ctx.external.registry.get(schema)?.id;
  323. if (!id)
  324. throw new Error("Schema is missing an `id` property");
  325. result.$id = ctx.external.uri(id);
  326. }
  327. Object.assign(result, root.def ?? root.schema);
  328. // build defs object
  329. const defs = ctx.external?.defs ?? {};
  330. for (const entry of ctx.seen.entries()) {
  331. const seen = entry[1];
  332. if (seen.def && seen.defId) {
  333. defs[seen.defId] = seen.def;
  334. }
  335. }
  336. // set definitions in result
  337. if (ctx.external) {
  338. }
  339. else {
  340. if (Object.keys(defs).length > 0) {
  341. if (ctx.target === "draft-2020-12") {
  342. result.$defs = defs;
  343. }
  344. else {
  345. result.definitions = defs;
  346. }
  347. }
  348. }
  349. try {
  350. // this "finalizes" this schema and ensures all cycles are removed
  351. // each call to finalize() is functionally independent
  352. // though the seen map is shared
  353. const finalized = JSON.parse(JSON.stringify(result));
  354. Object.defineProperty(finalized, "~standard", {
  355. value: {
  356. ...schema["~standard"],
  357. jsonSchema: {
  358. input: (0, exports.createStandardJSONSchemaMethod)(schema, "input", ctx.processors),
  359. output: (0, exports.createStandardJSONSchemaMethod)(schema, "output", ctx.processors),
  360. },
  361. },
  362. enumerable: false,
  363. writable: false,
  364. });
  365. return finalized;
  366. }
  367. catch (_err) {
  368. throw new Error("Error converting schema to JSON.");
  369. }
  370. }
  371. function isTransforming(_schema, _ctx) {
  372. const ctx = _ctx ?? { seen: new Set() };
  373. if (ctx.seen.has(_schema))
  374. return false;
  375. ctx.seen.add(_schema);
  376. const def = _schema._zod.def;
  377. if (def.type === "transform")
  378. return true;
  379. if (def.type === "array")
  380. return isTransforming(def.element, ctx);
  381. if (def.type === "set")
  382. return isTransforming(def.valueType, ctx);
  383. if (def.type === "lazy")
  384. return isTransforming(def.getter(), ctx);
  385. if (def.type === "promise" ||
  386. def.type === "optional" ||
  387. def.type === "nonoptional" ||
  388. def.type === "nullable" ||
  389. def.type === "readonly" ||
  390. def.type === "default" ||
  391. def.type === "prefault") {
  392. return isTransforming(def.innerType, ctx);
  393. }
  394. if (def.type === "intersection") {
  395. return isTransforming(def.left, ctx) || isTransforming(def.right, ctx);
  396. }
  397. if (def.type === "record" || def.type === "map") {
  398. return isTransforming(def.keyType, ctx) || isTransforming(def.valueType, ctx);
  399. }
  400. if (def.type === "pipe") {
  401. return isTransforming(def.in, ctx) || isTransforming(def.out, ctx);
  402. }
  403. if (def.type === "object") {
  404. for (const key in def.shape) {
  405. if (isTransforming(def.shape[key], ctx))
  406. return true;
  407. }
  408. return false;
  409. }
  410. if (def.type === "union") {
  411. for (const option of def.options) {
  412. if (isTransforming(option, ctx))
  413. return true;
  414. }
  415. return false;
  416. }
  417. if (def.type === "tuple") {
  418. for (const item of def.items) {
  419. if (isTransforming(item, ctx))
  420. return true;
  421. }
  422. if (def.rest && isTransforming(def.rest, ctx))
  423. return true;
  424. return false;
  425. }
  426. return false;
  427. }
  428. /**
  429. * Creates a toJSONSchema method for a schema instance.
  430. * This encapsulates the logic of initializing context, processing, extracting defs, and finalizing.
  431. */
  432. const createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
  433. const ctx = initializeContext({ ...params, processors });
  434. process(schema, ctx);
  435. extractDefs(ctx, schema);
  436. return finalize(ctx, schema);
  437. };
  438. exports.createToJSONSchemaMethod = createToJSONSchemaMethod;
  439. const createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) => {
  440. const { libraryOptions, target } = params ?? {};
  441. const ctx = initializeContext({ ...(libraryOptions ?? {}), target, io, processors });
  442. process(schema, ctx);
  443. extractDefs(ctx, schema);
  444. return finalize(ctx, schema);
  445. };
  446. exports.createStandardJSONSchemaMethod = createStandardJSONSchemaMethod;