@es-joy/jsoe 0.26.1 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/.gitignore +10 -0
  2. package/.npmignore +9 -0
  3. package/CHANGES.md +43 -0
  4. package/README.md +3 -3
  5. package/badges/coverage-badge.svg +1 -1
  6. package/dist/formats/schema.d.ts.map +1 -1
  7. package/dist/fundamentalTypes/arrayType.d.ts.map +1 -1
  8. package/dist/fundamentalTypes/stringType.d.ts.map +1 -1
  9. package/dist/index.js +2 -2
  10. package/dist/index.js.map +1 -1
  11. package/dist/typeChoices.d.ts.map +1 -1
  12. package/dist/types.d.ts.map +1 -1
  13. package/eslint.config.js +36 -27
  14. package/package.json +12 -11
  15. package/pnpm-workspace.yaml +3 -2
  16. package/src/formats/schema.js +35 -6
  17. package/src/fundamentalTypes/arrayType.js +30 -6
  18. package/src/fundamentalTypes/stringType.js +5 -0
  19. package/src/typeChoices.js +3 -1
  20. package/src/types.js +7 -0
  21. package/vendor/typeson-registry/dist/index.js +83 -10
  22. package/vendor/zod/classic/checks.d.ts +1 -1
  23. package/vendor/zod/classic/checks.js +1 -1
  24. package/vendor/zod/classic/external.d.ts +1 -1
  25. package/vendor/zod/classic/external.js +1 -1
  26. package/vendor/zod/classic/from-json-schema.js +245 -24
  27. package/vendor/zod/classic/schemas.d.ts +17 -1
  28. package/vendor/zod/classic/schemas.js +77 -29
  29. package/vendor/zod/core/api.d.ts +6 -4
  30. package/vendor/zod/core/api.js +17 -2
  31. package/vendor/zod/core/checks.d.ts +1 -1
  32. package/vendor/zod/core/checks.js +0 -96
  33. package/vendor/zod/core/compile.d.ts +18 -4
  34. package/vendor/zod/core/compile.js +146 -67
  35. package/vendor/zod/core/doc.js +7 -2
  36. package/vendor/zod/core/errors.js +2 -5
  37. package/vendor/zod/core/json-schema-generator.js +2 -2
  38. package/vendor/zod/core/json-schema-processors.d.ts +17 -0
  39. package/vendor/zod/core/json-schema-processors.js +195 -64
  40. package/vendor/zod/core/memoizer.js +72 -23
  41. package/vendor/zod/core/parse.js +37 -16
  42. package/vendor/zod/core/regexes.d.ts +2 -0
  43. package/vendor/zod/core/regexes.js +8 -4
  44. package/vendor/zod/core/schemas.d.ts +42 -7
  45. package/vendor/zod/core/schemas.js +301 -94
  46. package/vendor/zod/core/to-json-schema.d.ts +7 -3
  47. package/vendor/zod/core/to-json-schema.js +8 -7
  48. package/vendor/zod/core/util.d.ts +25 -4
  49. package/vendor/zod/core/util.js +160 -143
  50. package/vendor/zod/core/versions.d.ts +1 -1
  51. package/vendor/zod/core/versions.js +2 -2
  52. package/vendor/zod/core/visit.js +12 -0
  53. package/vendor/zod/locales/ar.js +1 -0
  54. package/vendor/zod/locales/az.js +1 -0
  55. package/vendor/zod/locales/be.js +1 -0
  56. package/vendor/zod/locales/bg.js +1 -0
  57. package/vendor/zod/locales/bn.js +1 -0
  58. package/vendor/zod/locales/ca.js +1 -0
  59. package/vendor/zod/locales/ckb.js +1 -0
  60. package/vendor/zod/locales/cs.js +1 -0
  61. package/vendor/zod/locales/da.js +1 -0
  62. package/vendor/zod/locales/de.js +1 -0
  63. package/vendor/zod/locales/el.js +1 -0
  64. package/vendor/zod/locales/en.js +1 -0
  65. package/vendor/zod/locales/eo.js +1 -0
  66. package/vendor/zod/locales/es.js +1 -0
  67. package/vendor/zod/locales/fa.js +1 -0
  68. package/vendor/zod/locales/fi.js +1 -0
  69. package/vendor/zod/locales/fr-CA.js +1 -0
  70. package/vendor/zod/locales/fr.js +1 -0
  71. package/vendor/zod/locales/gu.js +1 -0
  72. package/vendor/zod/locales/he.js +1 -0
  73. package/vendor/zod/locales/hi.js +1 -0
  74. package/vendor/zod/locales/hr.js +1 -0
  75. package/vendor/zod/locales/hu.js +1 -0
  76. package/vendor/zod/locales/hy.js +1 -0
  77. package/vendor/zod/locales/id.js +1 -0
  78. package/vendor/zod/locales/index.d.ts +1 -0
  79. package/vendor/zod/locales/index.js +1 -0
  80. package/vendor/zod/locales/is.js +1 -0
  81. package/vendor/zod/locales/it.js +1 -0
  82. package/vendor/zod/locales/ja.js +1 -0
  83. package/vendor/zod/locales/ka.js +1 -0
  84. package/vendor/zod/locales/km.js +1 -0
  85. package/vendor/zod/locales/kn.js +1 -0
  86. package/vendor/zod/locales/ko.js +1 -0
  87. package/vendor/zod/locales/lt.js +1 -0
  88. package/vendor/zod/locales/mk.js +1 -0
  89. package/vendor/zod/locales/ms.js +1 -0
  90. package/vendor/zod/locales/ne.js +1 -0
  91. package/vendor/zod/locales/nl.js +1 -0
  92. package/vendor/zod/locales/nn.js +1 -0
  93. package/vendor/zod/locales/no.js +1 -0
  94. package/vendor/zod/locales/ota.js +1 -0
  95. package/vendor/zod/locales/pl.js +1 -0
  96. package/vendor/zod/locales/ps.js +1 -0
  97. package/vendor/zod/locales/pt-BR.js +1 -0
  98. package/vendor/zod/locales/pt.js +1 -0
  99. package/vendor/zod/locales/ro.js +1 -0
  100. package/vendor/zod/locales/ru.js +1 -0
  101. package/vendor/zod/locales/sk.js +1 -0
  102. package/vendor/zod/locales/sl.js +1 -0
  103. package/vendor/zod/locales/sv.js +1 -0
  104. package/vendor/zod/locales/ta.js +1 -0
  105. package/vendor/zod/locales/tg.d.ts +4 -0
  106. package/vendor/zod/locales/tg.js +116 -0
  107. package/vendor/zod/locales/th.js +1 -0
  108. package/vendor/zod/locales/tk.js +1 -0
  109. package/vendor/zod/locales/tr.js +1 -0
  110. package/vendor/zod/locales/uk.js +1 -0
  111. package/vendor/zod/locales/ur.js +1 -0
  112. package/vendor/zod/locales/uz.js +1 -0
  113. package/vendor/zod/locales/vi.js +1 -0
  114. package/vendor/zod/locales/yo.js +1 -0
  115. package/vendor/zod/locales/zh-CN.js +1 -0
  116. package/vendor/zod/locales/zh-TW.js +1 -0
  117. package/vendor/zod/mini/checks.d.ts +1 -1
  118. package/vendor/zod/mini/checks.js +1 -1
  119. package/vendor/zod/mini/external.d.ts +1 -1
  120. package/vendor/zod/mini/external.js +1 -1
  121. package/vendor/zod/mini/schemas.d.ts +8 -0
  122. package/vendor/zod/mini/schemas.js +19 -2
  123. package/vendor/zodexy/dist/esm/index.js +34 -1
  124. package/vendor/zodexy/dist/schema.zodexy.json +18 -0
@@ -20,7 +20,6 @@ const _messageDesc = {
20
20
  enumerable: true,
21
21
  configurable: true,
22
22
  };
23
- const _zodDesc = { value: undefined, enumerable: false };
24
23
  const _issuesDesc = { value: undefined, enumerable: false };
25
24
  /* Prototypes that already carry the lazy `toString`. Seeded with the
26
25
  * intrinsics so that `init` on a foreign object — it accepts any object —
@@ -28,12 +27,10 @@ const _issuesDesc = { value: undefined, enumerable: false };
28
27
  const _installedToString = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);
29
28
  const initializer = (inst, def) => {
30
29
  inst.name = "$ZodError";
31
- _zodDesc.value = inst._zod;
32
- Object.defineProperty(inst, "_zod", _zodDesc);
30
+ // `_zod` is already non-enumerable: $constructor's init defined it with this same descriptor
33
31
  _issuesDesc.value = def;
34
32
  Object.defineProperty(inst, "issues", _issuesDesc);
35
- // Clear the shared slots; a retained `value` pins the last error's issues.
36
- _zodDesc.value = undefined;
33
+ // Clear the shared slot; a retained `value` pins the last error's issues.
37
34
  _issuesDesc.value = undefined;
38
35
  Object.defineProperty(inst, "message", _messageDesc);
39
36
  /* `toString` lives as a non-enumerable lazy getter on the shared
@@ -1,5 +1,5 @@
1
1
  import { allProcessors } from "./json-schema-processors.js";
2
- import { extractDefs, finalize, initializeContext, process, } from "./to-json-schema.js";
2
+ import { extractDefs, finalize, initializeContext, processSchema, } from "./to-json-schema.js";
3
3
  /**
4
4
  * Legacy class-based interface for JSON Schema generation.
5
5
  * This class wraps the new functional implementation to provide backward compatibility.
@@ -71,7 +71,7 @@ export class JSONSchemaGenerator {
71
71
  * This must be called before emit().
72
72
  */
73
73
  process(schema, _params = { path: [], schemaPath: [] }) {
74
- return process(schema, this.ctx, _params);
74
+ return processSchema(schema, this.ctx, _params);
75
75
  }
76
76
  /**
77
77
  * Emit the final JSON Schema after processing.
@@ -1,6 +1,21 @@
1
1
  import type { $ZodRegistry } from "./registries.js";
2
2
  import type * as schemas from "./schemas.js";
3
3
  import { type Processor, type RegistryToJSONSchemaParams, type ToJSONSchemaParams, type ZodStandardJSONSchemaPayload } from "./to-json-schema.js";
4
+ type Bound = number | bigint | Date;
5
+ interface CheckAggregate<N extends Bound = Bound> {
6
+ minimum?: N;
7
+ maximum?: N;
8
+ exclusiveMinimum?: N;
9
+ exclusiveMaximum?: N;
10
+ multipleOf?: number[];
11
+ format?: string;
12
+ isInt?: boolean;
13
+ patterns?: Set<RegExp>;
14
+ mime?: string[];
15
+ contentEncoding?: string;
16
+ laxFormat?: boolean;
17
+ }
18
+ export declare function aggregateChecks<N extends Bound = Bound>(schema: schemas.$ZodType): CheckAggregate<N>;
4
19
  export declare const stringProcessor: Processor<schemas.$ZodString>;
5
20
  export declare const numberProcessor: Processor<schemas.$ZodNumber>;
6
21
  export declare const booleanProcessor: Processor<schemas.$ZodBoolean>;
@@ -26,6 +41,7 @@ export declare const mapProcessor: Processor<schemas.$ZodMap>;
26
41
  export declare const setProcessor: Processor<schemas.$ZodSet>;
27
42
  export declare const arrayProcessor: Processor<schemas.$ZodArray>;
28
43
  export declare const objectProcessor: Processor<schemas.$ZodObject>;
44
+ export declare const propertiesProcessor: Processor<schemas.$ZodProperties>;
29
45
  export declare const unionProcessor: Processor<schemas.$ZodUnion>;
30
46
  export declare const intersectionProcessor: Processor<schemas.$ZodIntersection>;
31
47
  export declare const tupleProcessor: Processor<schemas.$ZodTuple>;
@@ -47,3 +63,4 @@ export declare function toJSONSchema(registry: $ZodRegistry<{
47
63
  }>, params?: RegistryToJSONSchemaParams): {
48
64
  schemas: Record<string, ZodStandardJSONSchemaPayload<schemas.$ZodType>>;
49
65
  };
66
+ export {};
@@ -1,6 +1,100 @@
1
1
  import * as regexes from "./regexes.js";
2
- import { extractDefs, finalize, handleUnrepresentable, initializeContext, process, } from "./to-json-schema.js";
3
- import { assignProp, getEnumValues } from "./util.js";
2
+ import { base64Charset, base64urlCharset } from "./schemas.js";
3
+ import { extractDefs, finalize, handleUnrepresentable, initializeContext, isTransforming, processSchema, } from "./to-json-schema.js";
4
+ import { BIGINT_FORMAT_RANGES, NUMBER_FORMAT_RANGES, assignProp, getEnumValues } from "./util.js";
5
+ const narrowMin = (agg, key, value) => {
6
+ if (agg[key] === undefined || value > agg[key])
7
+ agg[key] = value;
8
+ };
9
+ const narrowMax = (agg, key, value) => {
10
+ if (agg[key] === undefined || value < agg[key])
11
+ agg[key] = value;
12
+ };
13
+ const narrowBoth = (agg, value) => {
14
+ narrowMin(agg, "minimum", value);
15
+ narrowMax(agg, "maximum", value);
16
+ };
17
+ const addDivisor = (agg, value) => {
18
+ agg.multipleOf ?? (agg.multipleOf = []);
19
+ if (!agg.multipleOf.includes(value))
20
+ agg.multipleOf.push(value);
21
+ };
22
+ const addPattern = (agg, pattern) => {
23
+ agg.patterns ?? (agg.patterns = new Set());
24
+ agg.patterns.add(pattern);
25
+ };
26
+ const intersectMime = (agg, mime) => {
27
+ agg.mime = agg.mime ? agg.mime.filter((m) => mime.includes(m)) : [...mime];
28
+ };
29
+ // last-wins, matching the bag's historical write order; the flag keeps an integer format from being lost to a later float one
30
+ const setFormat = (agg, format) => {
31
+ agg.format = format;
32
+ if (format.includes("int"))
33
+ agg.isInt = true;
34
+ };
35
+ const minContributor = (agg, def) => narrowMin(agg, "minimum", def.minimum);
36
+ const maxContributor = (agg, def) => narrowMax(agg, "maximum", def.maximum);
37
+ const formatContributor = (ranges) => (agg, def) => {
38
+ setFormat(agg, def.format);
39
+ const [minimum, maximum] = ranges[def.format];
40
+ narrowMin(agg, "minimum", minimum);
41
+ narrowMax(agg, "maximum", maximum);
42
+ };
43
+ const contributors = {
44
+ greater_than: (agg, def) => narrowMin(agg, def.inclusive ? "minimum" : "exclusiveMinimum", def.value),
45
+ less_than: (agg, def) => narrowMax(agg, def.inclusive ? "maximum" : "exclusiveMaximum", def.value),
46
+ multiple_of: (agg, def) => addDivisor(agg, def.value),
47
+ number_format: formatContributor(NUMBER_FORMAT_RANGES),
48
+ bigint_format: formatContributor(BIGINT_FORMAT_RANGES),
49
+ min_length: minContributor,
50
+ max_length: maxContributor,
51
+ length_equals: (agg, def) => narrowBoth(agg, def.length),
52
+ min_size: minContributor,
53
+ max_size: maxContributor,
54
+ size_equals: (agg, def) => narrowBoth(agg, def.size),
55
+ string_format: (agg, def) => {
56
+ setFormat(agg, def.format);
57
+ if (def.pattern)
58
+ addPattern(agg, def.pattern);
59
+ if (def.format === "base64" || def.format === "base64url")
60
+ agg.contentEncoding = def.format;
61
+ if (def.local || def.precision === -1)
62
+ agg.laxFormat = true;
63
+ },
64
+ mime_type: (agg, def) => intersectMime(agg, def.mime),
65
+ };
66
+ export function aggregateChecks(schema) {
67
+ const agg = {};
68
+ const def = schema._zod.def;
69
+ // a format schema is its own first check, same rule as $ZodType init
70
+ const list = schema._zod.traits.has("$ZodCheck")
71
+ ? [schema, ...(def.checks ?? [])]
72
+ : (def.checks ?? []);
73
+ for (const ch of list)
74
+ contributors[ch._zod.def.check]?.(agg, ch._zod.def);
75
+ // 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
76
+ const bag = schema._zod.bag;
77
+ if (bag.minimum !== undefined)
78
+ narrowMin(agg, "minimum", bag.minimum);
79
+ if (bag.exclusiveMinimum !== undefined)
80
+ narrowMin(agg, "exclusiveMinimum", bag.exclusiveMinimum);
81
+ if (bag.maximum !== undefined)
82
+ narrowMax(agg, "maximum", bag.maximum);
83
+ if (bag.exclusiveMaximum !== undefined)
84
+ narrowMax(agg, "exclusiveMaximum", bag.exclusiveMaximum);
85
+ if (bag.multipleOf !== undefined)
86
+ addDivisor(agg, bag.multipleOf);
87
+ if (bag.format !== undefined) {
88
+ agg.format ?? (agg.format = bag.format);
89
+ if (bag.format.includes("int"))
90
+ agg.isInt = true;
91
+ }
92
+ if (bag.mime)
93
+ intersectMime(agg, bag.mime);
94
+ for (const pattern of bag.patterns ?? [])
95
+ addPattern(agg, pattern);
96
+ return agg;
97
+ }
4
98
  const formatMap = {
5
99
  guid: "uuid",
6
100
  url: "uri",
@@ -9,11 +103,16 @@ const formatMap = {
9
103
  regex: "", // do not set
10
104
  };
11
105
  // ==================== SIMPLE TYPE PROCESSORS ====================
106
+ // 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
107
+ const exactPatterns = new Map([
108
+ [base64Charset, regexes.base64],
109
+ [base64urlCharset, regexes.base64url],
110
+ ]);
111
+ const exactPattern = (p) => exactPatterns.get(p) ?? p;
12
112
  export const stringProcessor = (schema, ctx, _json, _params) => {
13
113
  const json = _json;
14
114
  json.type = "string";
15
- const { minimum, maximum, format, patterns, contentEncoding, laxFormat } = schema._zod
16
- .bag;
115
+ const { minimum, maximum, format, patterns, contentEncoding, laxFormat } = aggregateChecks(schema);
17
116
  if (typeof minimum === "number")
18
117
  json.minLength = minimum;
19
118
  if (typeof maximum === "number")
@@ -31,7 +130,7 @@ export const stringProcessor = (schema, ctx, _json, _params) => {
31
130
  if (contentEncoding)
32
131
  json.contentEncoding = contentEncoding;
33
132
  if (patterns && patterns.size > 0) {
34
- const patternList = [...patterns];
133
+ const patternList = [...patterns].map(exactPattern);
35
134
  if (patternList.length === 1)
36
135
  json.pattern = patternList[0].source;
37
136
  else if (patternList.length > 1) {
@@ -48,11 +147,8 @@ export const stringProcessor = (schema, ctx, _json, _params) => {
48
147
  };
49
148
  export const numberProcessor = (schema, ctx, _json, params) => {
50
149
  const json = _json;
51
- const { minimum, maximum, format, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag;
52
- if (typeof format === "string" && format.includes("int"))
53
- json.type = "integer";
54
- else
55
- json.type = "number";
150
+ const { minimum, maximum, multipleOf, exclusiveMaximum, exclusiveMinimum, isInt } = aggregateChecks(schema);
151
+ json.type = isInt ? "integer" : "number";
56
152
  // when both minimum and exclusiveMinimum exist, pick the more restrictive one
57
153
  const exMin = typeof exclusiveMinimum === "number" && exclusiveMinimum >= (minimum ?? Number.NEGATIVE_INFINITY);
58
154
  const exMax = typeof exclusiveMaximum === "number" && exclusiveMaximum <= (maximum ?? Number.POSITIVE_INFINITY);
@@ -81,12 +177,21 @@ export const numberProcessor = (schema, ctx, _json, params) => {
81
177
  else if (typeof maximum === "number") {
82
178
  json.maximum = maximum;
83
179
  }
84
- if (typeof multipleOf === "number") {
180
+ if (multipleOf) {
85
181
  // 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.
86
- if (Number.isFinite(multipleOf) && multipleOf !== 0)
87
- json.multipleOf = Math.abs(multipleOf);
88
- else
89
- handleUnrepresentable(schema, ctx, json, params, `A multipleOf divisor of ${multipleOf} cannot be represented in JSON Schema`);
182
+ const divisors = new Set();
183
+ for (const divisor of multipleOf) {
184
+ if (Number.isFinite(divisor) && divisor !== 0)
185
+ divisors.add(Math.abs(divisor));
186
+ else
187
+ handleUnrepresentable(schema, ctx, json, params, `A multipleOf divisor of ${divisor} cannot be represented in JSON Schema`);
188
+ }
189
+ // chained divisors are a conjunction the keyword cannot carry alone, so extras ride an allOf, same as stacked patterns
190
+ const [first, ...rest] = divisors;
191
+ if (first !== undefined)
192
+ json.multipleOf = first;
193
+ if (rest.length)
194
+ json.allOf = [...(json.allOf ?? []), ...rest.map((m) => ({ multipleOf: m }))];
90
195
  }
91
196
  };
92
197
  export const booleanProcessor = (_schema, _ctx, json, _params) => {
@@ -203,29 +308,24 @@ export const templateLiteralProcessor = (schema, _ctx, json, _params) => {
203
308
  };
204
309
  export const fileProcessor = (schema, _ctx, json, _params) => {
205
310
  const _json = json;
206
- const file = {
207
- type: "string",
208
- format: "binary",
209
- contentEncoding: "binary",
210
- };
211
- const { minimum, maximum, mime } = schema._zod.bag;
311
+ _json.type = "string";
312
+ _json.format = "binary";
313
+ _json.contentEncoding = "binary";
314
+ const { minimum, maximum, mime } = aggregateChecks(schema);
212
315
  if (minimum !== undefined)
213
- file.minLength = minimum;
316
+ _json.minLength = minimum;
214
317
  if (maximum !== undefined)
215
- file.maxLength = maximum;
216
- if (mime) {
217
- if (mime.length === 1) {
218
- file.contentMediaType = mime[0];
219
- Object.assign(_json, file);
220
- }
221
- else {
222
- Object.assign(_json, file); // shared props at root
223
- _json.anyOf = mime.map((m) => ({ contentMediaType: m })); // only contentMediaType differs
224
- }
225
- }
226
- else {
227
- Object.assign(_json, file);
228
- }
318
+ _json.maxLength = maximum;
319
+ if (!mime)
320
+ return;
321
+ // 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: {}`
322
+ if (mime.length === 0)
323
+ _json.not = {};
324
+ else if (mime.length === 1)
325
+ _json.contentMediaType = mime[0];
326
+ // only contentMediaType differs, so the shared props stay at the root
327
+ else
328
+ _json.anyOf = mime.map((m) => ({ contentMediaType: m }));
229
329
  };
230
330
  export const successProcessor = (_schema, _ctx, json, _params) => {
231
331
  json.type = "boolean";
@@ -249,13 +349,13 @@ export const setProcessor = (schema, ctx, json, params) => {
249
349
  export const arrayProcessor = (schema, ctx, _json, params) => {
250
350
  const json = _json;
251
351
  const def = schema._zod.def;
252
- const { minimum, maximum } = schema._zod.bag;
352
+ const { minimum, maximum } = aggregateChecks(schema);
253
353
  if (typeof minimum === "number")
254
354
  json.minItems = minimum;
255
355
  if (typeof maximum === "number")
256
356
  json.maxItems = maximum;
257
357
  json.type = "array";
258
- json.items = process(def.element, ctx, {
358
+ json.items = processSchema(def.element, ctx, {
259
359
  ...params,
260
360
  path: [...params.path, "items"],
261
361
  });
@@ -289,7 +389,7 @@ export const objectProcessor = (schema, ctx, _json, params) => {
289
389
  json.properties = {};
290
390
  for (const key in shape) {
291
391
  // assignProp so a __proto__ key becomes an own property instead of hitting the inherited setter on the plain {} we build into
292
- assignProp(json.properties, key, process(shape[key], ctx, {
392
+ assignProp(json.properties, key, processSchema(shape[key], ctx, {
293
393
  ...params,
294
394
  path: [...params.path, "properties", key],
295
395
  }));
@@ -319,17 +419,48 @@ export const objectProcessor = (schema, ctx, _json, params) => {
319
419
  json.additionalProperties = false;
320
420
  }
321
421
  else if (def.catchall) {
322
- json.additionalProperties = process(def.catchall, ctx, {
422
+ json.additionalProperties = processSchema(def.catchall, ctx, {
323
423
  ...params,
324
424
  path: [...params.path, "additionalProperties"],
325
425
  });
326
426
  }
327
427
  };
428
+ // asserts named properties in place and passes everything else through, so no additionalProperties constraint is emitted
429
+ export const propertiesProcessor = (schema, ctx, _json, params) => {
430
+ const json = _json;
431
+ const def = schema._zod.def;
432
+ // dropping a symbol key silently would emit a schema that asserts less than this one does
433
+ if (Object.getOwnPropertySymbols(def.shape).length &&
434
+ handleUnrepresentable(schema, ctx, json, params, "Symbol keys cannot be represented in JSON Schema")) {
435
+ return;
436
+ }
437
+ // the parsed value is the input, so an output-mode emission would describe a transformed value this schema never returns
438
+ if (ctx.io === "output") {
439
+ for (const key in def.shape) {
440
+ if (isTransforming(def.shape[key]) &&
441
+ handleUnrepresentable(schema, ctx, json, params, `z.properties() returns its input, so the output of a transforming schema at key "${key}" cannot be represented in JSON Schema`)) {
442
+ return;
443
+ }
444
+ }
445
+ }
446
+ json.type = "object";
447
+ json.properties = {};
448
+ for (const key in def.shape) {
449
+ assignProp(json.properties, key, processSchema(def.shape[key], ctx, {
450
+ ...params,
451
+ path: [...params.path, "properties", key],
452
+ }));
453
+ }
454
+ // input-side optionality in both modes: the parsed value is the input, so a defaulted key that stays absent must not be required of the output either
455
+ const required = Object.keys(def.shape).filter((key) => inputOptin(def.shape[key]) === undefined);
456
+ if (required.length > 0)
457
+ json.required = required;
458
+ };
328
459
  export const unionProcessor = (schema, ctx, json, params) => {
329
460
  const def = schema._zod.def;
330
461
  // Exclusive unions (inclusive === false) use oneOf (exactly one match) instead of anyOf (one or more matches). This includes both z.xor() and discriminated unions
331
462
  const isExclusive = def.inclusive === false;
332
- const options = def.options.map((x, i) => process(x, ctx, {
463
+ const options = def.options.map((x, i) => processSchema(x, ctx, {
333
464
  ...params,
334
465
  path: [...params.path, isExclusive ? "oneOf" : "anyOf", i],
335
466
  }));
@@ -342,11 +473,11 @@ export const unionProcessor = (schema, ctx, json, params) => {
342
473
  };
343
474
  export const intersectionProcessor = (schema, ctx, json, params) => {
344
475
  const def = schema._zod.def;
345
- const a = process(def.left, ctx, {
476
+ const a = processSchema(def.left, ctx, {
346
477
  ...params,
347
478
  path: [...params.path, "allOf", 0],
348
479
  });
349
- const b = process(def.right, ctx, {
480
+ const b = processSchema(def.right, ctx, {
350
481
  ...params,
351
482
  path: [...params.path, "allOf", 1],
352
483
  });
@@ -365,12 +496,12 @@ export const tupleProcessor = (schema, ctx, _json, params) => {
365
496
  json.type = "array";
366
497
  const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
367
498
  const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
368
- const prefixItems = def.items.map((x, i) => process(x, ctx, {
499
+ const prefixItems = def.items.map((x, i) => processSchema(x, ctx, {
369
500
  ...params,
370
501
  path: [...params.path, prefixPath, i],
371
502
  }));
372
503
  const rest = def.rest
373
- ? process(def.rest, ctx, {
504
+ ? processSchema(def.rest, ctx, {
374
505
  ...params,
375
506
  path: [...params.path, restPath, ...(ctx.target === "openapi-3.0" ? [def.items.length] : [])],
376
507
  })
@@ -424,7 +555,7 @@ export const tupleProcessor = (schema, ctx, _json, params) => {
424
555
  json.maxItems = maxItems;
425
556
  }
426
557
  // explicit user-defined length checks take precedence
427
- const { minimum, maximum } = schema._zod.bag;
558
+ const { minimum, maximum } = aggregateChecks(schema);
428
559
  if (typeof minimum === "number")
429
560
  json.minItems = minimum;
430
561
  if (typeof maximum === "number")
@@ -517,23 +648,22 @@ export const recordProcessor = (schema, ctx, _json, params) => {
517
648
  json.type = "object";
518
649
  // For looseRecord with regex patterns, use patternProperties. This correctly represents "only validate keys matching the pattern" semantics and composes well with allOf (intersections)
519
650
  const keyType = def.keyType;
520
- const keyBag = keyType._zod.bag;
521
- const patterns = keyBag?.patterns;
651
+ const patterns = aggregateChecks(keyType).patterns;
522
652
  if (def.mode === "loose" && patterns && patterns.size > 0) {
523
653
  // Use patternProperties for looseRecord with regex patterns
524
- const valueSchema = process(def.valueType, ctx, {
654
+ const valueSchema = processSchema(def.valueType, ctx, {
525
655
  ...params,
526
656
  path: [...params.path, "patternProperties", "*"],
527
657
  });
528
658
  json.patternProperties = {};
529
659
  for (const pattern of patterns) {
530
- assignProp(json.patternProperties, pattern.source, valueSchema);
660
+ assignProp(json.patternProperties, exactPattern(pattern).source, valueSchema);
531
661
  }
532
662
  }
533
663
  else {
534
664
  // Default behavior: use propertyNames + additionalProperties
535
665
  if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
536
- json.propertyNames = process(def.keyType, ctx, {
666
+ json.propertyNames = processSchema(def.keyType, ctx, {
537
667
  ...params,
538
668
  path: [...params.path, "propertyNames"],
539
669
  });
@@ -545,7 +675,7 @@ export const recordProcessor = (schema, ctx, _json, params) => {
545
675
  }
546
676
  pending.push(schema);
547
677
  }
548
- json.additionalProperties = process(def.valueType, ctx, {
678
+ json.additionalProperties = processSchema(def.valueType, ctx, {
549
679
  ...params,
550
680
  path: [...params.path, "additionalProperties"],
551
681
  });
@@ -563,7 +693,7 @@ export const recordProcessor = (schema, ctx, _json, params) => {
563
693
  };
564
694
  export const nullableProcessor = (schema, ctx, json, params) => {
565
695
  const def = schema._zod.def;
566
- const inner = process(def.innerType, ctx, params);
696
+ const inner = processSchema(def.innerType, ctx, params);
567
697
  const seen = ctx.seen.get(schema);
568
698
  if (ctx.target === "openapi-3.0") {
569
699
  seen.ref = def.innerType;
@@ -575,7 +705,7 @@ export const nullableProcessor = (schema, ctx, json, params) => {
575
705
  };
576
706
  export const nonoptionalProcessor = (schema, ctx, _json, params) => {
577
707
  const def = schema._zod.def;
578
- process(def.innerType, ctx, params);
708
+ processSchema(def.innerType, ctx, params);
579
709
  const seen = ctx.seen.get(schema);
580
710
  seen.ref = def.innerType;
581
711
  };
@@ -598,7 +728,7 @@ function serializeDefaultValue(value, schema, ctx, json, params) {
598
728
  }
599
729
  export const defaultProcessor = (schema, ctx, json, params) => {
600
730
  const def = schema._zod.def;
601
- process(def.innerType, ctx, params);
731
+ processSchema(def.innerType, ctx, params);
602
732
  const seen = ctx.seen.get(schema);
603
733
  seen.ref = def.innerType;
604
734
  const value = serializeDefaultValue(def.defaultValue, schema, ctx, json, params);
@@ -607,7 +737,7 @@ export const defaultProcessor = (schema, ctx, json, params) => {
607
737
  };
608
738
  export const prefaultProcessor = (schema, ctx, json, params) => {
609
739
  const def = schema._zod.def;
610
- process(def.innerType, ctx, params);
740
+ processSchema(def.innerType, ctx, params);
611
741
  const seen = ctx.seen.get(schema);
612
742
  seen.ref = def.innerType;
613
743
  if (ctx.io !== "input")
@@ -618,7 +748,7 @@ export const prefaultProcessor = (schema, ctx, json, params) => {
618
748
  };
619
749
  export const catchProcessor = (schema, ctx, json, params) => {
620
750
  const def = schema._zod.def;
621
- process(def.innerType, ctx, params);
751
+ processSchema(def.innerType, ctx, params);
622
752
  const seen = ctx.seen.get(schema);
623
753
  seen.ref = def.innerType;
624
754
  let catchValue;
@@ -635,32 +765,32 @@ export const pipeProcessor = (schema, ctx, _json, params) => {
635
765
  const def = schema._zod.def;
636
766
  const inIsTransform = def.in._zod.traits.has("$ZodTransform");
637
767
  const innerType = ctx.io === "input" ? (inIsTransform ? def.out : def.in) : def.out;
638
- process(innerType, ctx, params);
768
+ processSchema(innerType, ctx, params);
639
769
  const seen = ctx.seen.get(schema);
640
770
  seen.ref = innerType;
641
771
  };
642
772
  export const readonlyProcessor = (schema, ctx, json, params) => {
643
773
  const def = schema._zod.def;
644
- process(def.innerType, ctx, params);
774
+ processSchema(def.innerType, ctx, params);
645
775
  const seen = ctx.seen.get(schema);
646
776
  seen.ref = def.innerType;
647
777
  json.readOnly = true;
648
778
  };
649
779
  export const promiseProcessor = (schema, ctx, _json, params) => {
650
780
  const def = schema._zod.def;
651
- process(def.innerType, ctx, params);
781
+ processSchema(def.innerType, ctx, params);
652
782
  const seen = ctx.seen.get(schema);
653
783
  seen.ref = def.innerType;
654
784
  };
655
785
  export const optionalProcessor = (schema, ctx, _json, params) => {
656
786
  const def = schema._zod.def;
657
- process(def.innerType, ctx, params);
787
+ processSchema(def.innerType, ctx, params);
658
788
  const seen = ctx.seen.get(schema);
659
789
  seen.ref = def.innerType;
660
790
  };
661
791
  export const lazyProcessor = (schema, ctx, _json, params) => {
662
792
  const innerType = schema._zod.innerType;
663
- process(innerType, ctx, params);
793
+ processSchema(innerType, ctx, params);
664
794
  const seen = ctx.seen.get(schema);
665
795
  seen.ref = innerType;
666
796
  };
@@ -685,6 +815,7 @@ export const allProcessors = {
685
815
  file: fileProcessor,
686
816
  success: successProcessor,
687
817
  custom: customProcessor,
818
+ properties: propertiesProcessor,
688
819
  function: functionProcessor,
689
820
  transform: transformProcessor,
690
821
  map: mapProcessor,
@@ -715,7 +846,7 @@ export function toJSONSchema(input, params) {
715
846
  // First pass: process all schemas to build the seen map
716
847
  for (const entry of registry._idmap.entries()) {
717
848
  const [_, schema] = entry;
718
- process(schema, ctx);
849
+ processSchema(schema, ctx);
719
850
  }
720
851
  const schemas = {};
721
852
  const external = {
@@ -741,7 +872,7 @@ export function toJSONSchema(input, params) {
741
872
  }
742
873
  // Single schema case
743
874
  const ctx = initializeContext({ ...params, processors: allProcessors });
744
- process(input, ctx);
875
+ processSchema(input, ctx);
745
876
  extractDefs(ctx, input);
746
877
  return finalize(ctx, input);
747
878
  }