@gabrielbryk/json-schema-to-zod 2.12.1 → 2.14.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 (57) hide show
  1. package/.github/RELEASE_SETUP.md +120 -0
  2. package/.github/TOOLING_GUIDE.md +169 -0
  3. package/.github/dependabot.yml +52 -0
  4. package/.github/workflows/ci.yml +33 -0
  5. package/.github/workflows/release.yml +12 -4
  6. package/.github/workflows/security.yml +40 -0
  7. package/.husky/commit-msg +1 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.lintstagedrc.json +3 -0
  10. package/.prettierrc +20 -0
  11. package/AGENTS.md +7 -0
  12. package/CHANGELOG.md +13 -4
  13. package/README.md +24 -9
  14. package/commitlint.config.js +24 -0
  15. package/createIndex.ts +4 -4
  16. package/dist/cli.js +3 -4
  17. package/dist/core/analyzeSchema.js +56 -11
  18. package/dist/core/emitZod.js +43 -12
  19. package/dist/generators/generateBundle.js +67 -92
  20. package/dist/index.js +1 -0
  21. package/dist/parsers/parseAllOf.js +11 -12
  22. package/dist/parsers/parseAnyOf.js +2 -2
  23. package/dist/parsers/parseArray.js +38 -12
  24. package/dist/parsers/parseMultipleType.js +2 -2
  25. package/dist/parsers/parseNumber.js +44 -102
  26. package/dist/parsers/parseObject.js +136 -443
  27. package/dist/parsers/parseOneOf.js +57 -110
  28. package/dist/parsers/parseSchema.js +176 -71
  29. package/dist/parsers/parseSimpleDiscriminatedOneOf.js +2 -2
  30. package/dist/parsers/parseString.js +113 -253
  31. package/dist/types/Types.d.ts +37 -1
  32. package/dist/types/core/analyzeSchema.d.ts +4 -0
  33. package/dist/types/generators/generateBundle.d.ts +1 -1
  34. package/dist/types/index.d.ts +1 -0
  35. package/dist/types/utils/schemaNaming.d.ts +6 -0
  36. package/dist/utils/cliTools.js +1 -2
  37. package/dist/utils/esmEmitter.js +6 -2
  38. package/dist/utils/extractInlineObject.js +1 -3
  39. package/dist/utils/jsdocs.js +1 -4
  40. package/dist/utils/liftInlineObjects.js +76 -15
  41. package/dist/utils/resolveRef.js +35 -10
  42. package/dist/utils/schemaNaming.js +31 -0
  43. package/dist/utils/schemaRepresentation.js +35 -66
  44. package/dist/zodToJsonSchema.js +1 -2
  45. package/docs/IMPROVEMENT-PLAN.md +30 -12
  46. package/docs/ZOD-V4-RECURSIVE-TYPE-LIMITATIONS.md +70 -25
  47. package/docs/proposals/allof-required-merging.md +10 -4
  48. package/docs/proposals/bundle-refactor.md +10 -4
  49. package/docs/proposals/discriminated-union-with-default.md +18 -14
  50. package/docs/proposals/inline-object-lifting.md +15 -5
  51. package/docs/proposals/ref-anchor-support.md +11 -0
  52. package/output.txt +67 -0
  53. package/package.json +18 -5
  54. package/scripts/generateWorkflowSchema.ts +5 -14
  55. package/scripts/regenerate_bundle.ts +25 -0
  56. package/tsc_output.txt +542 -0
  57. package/tsc_output_2.txt +489 -0
@@ -2,12 +2,39 @@ import { withMessage } from "../utils/withMessage.js";
2
2
  import { parseSchema } from "./parseSchema.js";
3
3
  import { anyOrUnknown } from "../utils/anyOrUnknown.js";
4
4
  export const parseArray = (schema, refs) => {
5
- if (Array.isArray(schema.items)) {
5
+ // JSON Schema 2020-12 uses `prefixItems` for tuples.
6
+ // Older drafts used `items` as an array.
7
+ const prefixItems = schema.prefixItems || (Array.isArray(schema.items) ? schema.items : undefined);
8
+ if (prefixItems) {
6
9
  // Tuple case
7
- const itemResults = schema.items.map((v, i) => parseSchema(v, { ...refs, path: [...refs.path, "items", i] }));
8
- let tuple = `z.tuple([${itemResults.map(r => r.expression).join(", ")}])`;
9
- const tupleTypes = itemResults.map(r => r.type).join(", ");
10
- let tupleType = `z.ZodTuple<[${tupleTypes}]>`;
10
+ const itemResults = prefixItems.map((v, i) => parseSchema(v, { ...refs, path: [...refs.path, "prefixItems", i] }));
11
+ let tuple = `z.tuple([${itemResults.map((r) => r.expression).join(", ")}])`;
12
+ // We construct the type manually for the tuple part
13
+ let tupleTypes = itemResults.map((r) => r.type).join(", ");
14
+ let tupleType = `z.ZodTuple<[${tupleTypes}], null>`; // Default null rest
15
+ // Handle "additionalItems" (older drafts) or "items" (2020-12 when prefixItems is used)
16
+ // If prefixItems is present, `items` acts as the schema for additional items.
17
+ // If prefixItems came from `items` (array form), then `additionalItems` controls the rest.
18
+ const additionalSchema = schema.prefixItems ? schema.items : schema.additionalItems;
19
+ if (additionalSchema === false) {
20
+ // Closed tuple
21
+ }
22
+ else if (additionalSchema) {
23
+ const restSchema = additionalSchema === true
24
+ ? anyOrUnknown(refs)
25
+ : parseSchema(additionalSchema, {
26
+ ...refs,
27
+ path: [...refs.path, "items"],
28
+ });
29
+ tuple += `.rest(${restSchema.expression})`;
30
+ tupleType = `z.ZodTuple<[${tupleTypes}], ${restSchema.type}>`;
31
+ }
32
+ else {
33
+ // Open by default
34
+ const anyRes = anyOrUnknown(refs);
35
+ tuple += `.rest(${anyRes.expression})`;
36
+ tupleType = `z.ZodTuple<[${tupleTypes}], ${anyRes.type}>`;
37
+ }
11
38
  if (schema.contains) {
12
39
  const containsResult = parseSchema(schema.contains, {
13
40
  ...refs,
@@ -24,18 +51,18 @@ export const parseArray = (schema, refs) => {
24
51
  ctx.addIssue({ code: "custom", message: "Array contains too many matching items" });
25
52
  }
26
53
  })`;
27
- // In Zod v4, .superRefine() doesn't change the type
28
54
  }
29
55
  return {
30
56
  expression: tuple,
31
57
  type: tupleType,
32
58
  };
33
59
  }
34
- // Array case
60
+ // Regular Array case
61
+ const itemsSchema = schema.items;
35
62
  const anyOrUnknownResult = anyOrUnknown(refs);
36
- const itemResult = !schema.items
63
+ const itemResult = !itemsSchema || itemsSchema === true
37
64
  ? anyOrUnknownResult
38
- : parseSchema(schema.items, {
65
+ : parseSchema(itemsSchema, {
39
66
  ...refs,
40
67
  path: [...refs.path, "items"],
41
68
  });
@@ -44,13 +71,13 @@ export const parseArray = (schema, refs) => {
44
71
  r += withMessage(schema, "minItems", ({ json }) => ({
45
72
  opener: `.min(${json}`,
46
73
  closer: ")",
47
- messagePrefix: ", { error: ",
74
+ messagePrefix: ", { message: ",
48
75
  messageCloser: " })",
49
76
  }));
50
77
  r += withMessage(schema, "maxItems", ({ json }) => ({
51
78
  opener: `.max(${json}`,
52
79
  closer: ")",
53
- messagePrefix: ", { error: ",
80
+ messagePrefix: ", { message: ",
54
81
  messageCloser: " })",
55
82
  }));
56
83
  if (schema.uniqueItems === true) {
@@ -94,7 +121,6 @@ export const parseArray = (schema, refs) => {
94
121
  }
95
122
  })`;
96
123
  }
97
- // In Zod v4, .superRefine() doesn't change the type, so no wrapping needed
98
124
  return {
99
125
  expression: r,
100
126
  type: arrayType,
@@ -1,8 +1,8 @@
1
1
  import { parseSchema } from "./parseSchema.js";
2
2
  export const parseMultipleType = (schema, refs) => {
3
3
  const schemas = schema.type.map((type) => parseSchema({ ...schema, type }, { ...refs, withoutDefaults: true }));
4
- const expressions = schemas.map(s => s.expression).join(", ");
5
- const types = schemas.map(s => s.type).join(", ");
4
+ const expressions = schemas.map((s) => s.expression).join(", ");
5
+ const types = schemas.map((s) => s.type).join(", ");
6
6
  return {
7
7
  expression: `z.union([${expressions}])`,
8
8
  type: `z.ZodUnion<[${types}]>`,
@@ -1,115 +1,57 @@
1
1
  import { withMessage } from "../utils/withMessage.js";
2
2
  export const parseNumber = (schema) => {
3
- const formatError = schema.errorMessage?.format;
4
- const numericFormatMap = {
5
- int32: "z.int32",
6
- uint32: "z.uint32",
7
- float32: "z.float32",
8
- float64: "z.float64",
9
- safeint: "z.safeint",
10
- int64: "z.int64",
11
- uint64: "z.uint64",
3
+ const formatMessage = schema.errorMessage?.format;
4
+ const formatParams = formatMessage ? `{ message: ${JSON.stringify(formatMessage)} }` : "";
5
+ const formatMap = {
6
+ int32: { expression: `z.int32(${formatParams})`, type: "z.ZodNumber" },
7
+ uint32: { expression: `z.uint32(${formatParams})`, type: "z.ZodNumber" },
8
+ float32: { expression: `z.float32(${formatParams})`, type: "z.ZodNumber" },
9
+ float64: { expression: `z.float64(${formatParams})`, type: "z.ZodNumber" },
10
+ safeint: { expression: `z.safeint(${formatParams})`, type: "z.ZodNumber" },
11
+ int64: { expression: `z.int64(${formatParams})`, type: "z.ZodBigInt" },
12
+ uint64: { expression: `z.uint64(${formatParams})`, type: "z.ZodBigInt" },
12
13
  };
13
- const mappedFormat = schema.format && numericFormatMap[schema.format] ? numericFormatMap[schema.format] : undefined;
14
- const formatParams = formatError !== undefined ? `{ error: ${JSON.stringify(formatError)} }` : "";
15
- let r = mappedFormat ? `${mappedFormat}(${formatParams})` : "z.number()";
16
- if (schema.type === "integer") {
17
- if (!mappedFormat) {
18
- r += withMessage(schema, "type", () => ({
19
- opener: ".int(",
20
- closer: ")",
21
- messagePrefix: "{ error: ",
22
- messageCloser: " })",
23
- }));
24
- }
14
+ let r = schema.type === "integer" ? "z.int()" : "z.number()";
15
+ let zodType = schema.type === "integer" ? "z.ZodInt" : "z.ZodNumber";
16
+ if (schema.format && formatMap[schema.format]) {
17
+ const mapped = formatMap[schema.format];
18
+ r = mapped.expression;
19
+ zodType = mapped.type;
25
20
  }
26
- else {
27
- if (!mappedFormat) {
28
- r += withMessage(schema, "format", ({ value }) => {
29
- if (value === "int64") {
30
- return {
31
- opener: ".int(",
32
- closer: ")",
33
- messagePrefix: "{ error: ",
34
- messageCloser: " })",
35
- };
36
- }
37
- });
38
- }
21
+ r += withMessage(schema, "multipleOf", ({ json }) => ({
22
+ opener: `.multipleOf(${json}`,
23
+ closer: ")",
24
+ messagePrefix: ", { message: ",
25
+ messageCloser: " })",
26
+ }));
27
+ const minimum = schema.minimum;
28
+ const maximum = schema.maximum;
29
+ const exclusiveMinimum = schema.exclusiveMinimum;
30
+ const exclusiveMaximum = schema.exclusiveMaximum;
31
+ const minMessage = schema.errorMessage?.minimum;
32
+ const maxMessage = schema.errorMessage?.maximum;
33
+ const exclMinMessage = schema.errorMessage?.exclusiveMinimum;
34
+ const exclMaxMessage = schema.errorMessage?.exclusiveMaximum;
35
+ if (typeof exclusiveMinimum === "number") {
36
+ r += `.gt(${exclusiveMinimum}${exclMinMessage ? `, { message: ${JSON.stringify(exclMinMessage)} }` : ""})`;
39
37
  }
40
- r += withMessage(schema, "multipleOf", ({ value, json }) => {
41
- if (value === 1) {
42
- if (r.startsWith("z.number().int(")) {
43
- return;
44
- }
45
- return {
46
- opener: ".int(",
47
- closer: ")",
48
- messagePrefix: "{ error: ",
49
- messageCloser: " })",
50
- };
51
- }
52
- return {
53
- opener: `.multipleOf(${json}`,
54
- closer: ")",
55
- messagePrefix: ", { error: ",
56
- messageCloser: " })",
57
- };
58
- });
59
- if (typeof schema.minimum === "number") {
60
- if (schema.exclusiveMinimum === true) {
61
- r += withMessage(schema, "minimum", ({ json }) => ({
62
- opener: `.gt(${json}`,
63
- closer: ")",
64
- messagePrefix: ", { error: ",
65
- messageCloser: " })",
66
- }));
67
- }
68
- else {
69
- r += withMessage(schema, "minimum", ({ json }) => ({
70
- opener: `.gte(${json}`,
71
- closer: ")",
72
- messagePrefix: ", { error: ",
73
- messageCloser: " })",
74
- }));
75
- }
38
+ else if (exclusiveMinimum === true && typeof minimum === "number") {
39
+ r += `.gt(${minimum}${exclMinMessage ? `, { message: ${JSON.stringify(exclMinMessage)} }` : ""})`;
76
40
  }
77
- else if (typeof schema.exclusiveMinimum === "number") {
78
- r += withMessage(schema, "exclusiveMinimum", ({ json }) => ({
79
- opener: `.gt(${json}`,
80
- closer: ")",
81
- messagePrefix: ", { error: ",
82
- messageCloser: " })",
83
- }));
41
+ else if (typeof minimum === "number") {
42
+ r += `.min(${minimum}${minMessage ? `, { message: ${JSON.stringify(minMessage)} }` : ""})`;
84
43
  }
85
- if (typeof schema.maximum === "number") {
86
- if (schema.exclusiveMaximum === true) {
87
- r += withMessage(schema, "maximum", ({ json }) => ({
88
- opener: `.lt(${json}`,
89
- closer: ")",
90
- messagePrefix: ", { error: ",
91
- messageCloser: " })",
92
- }));
93
- }
94
- else {
95
- r += withMessage(schema, "maximum", ({ json }) => ({
96
- opener: `.lte(${json}`,
97
- closer: ")",
98
- messagePrefix: ", { error: ",
99
- messageCloser: " })",
100
- }));
101
- }
44
+ if (typeof exclusiveMaximum === "number") {
45
+ r += `.lt(${exclusiveMaximum}${exclMaxMessage ? `, { message: ${JSON.stringify(exclMaxMessage)} }` : ""})`;
102
46
  }
103
- else if (typeof schema.exclusiveMaximum === "number") {
104
- r += withMessage(schema, "exclusiveMaximum", ({ json }) => ({
105
- opener: `.lt(${json}`,
106
- closer: ")",
107
- messagePrefix: ", { error: ",
108
- messageCloser: " })",
109
- }));
47
+ else if (exclusiveMaximum === true && typeof maximum === "number") {
48
+ r += `.lt(${maximum}${exclMaxMessage ? `, { message: ${JSON.stringify(exclMaxMessage)} }` : ""})`;
49
+ }
50
+ else if (typeof maximum === "number") {
51
+ r += `.max(${maximum}${maxMessage ? `, { message: ${JSON.stringify(maxMessage)} }` : ""})`;
110
52
  }
111
53
  return {
112
54
  expression: r,
113
- type: "z.ZodNumber",
55
+ type: zodType,
114
56
  };
115
57
  };