@gabrielbryk/json-schema-to-zod 2.11.0 → 2.12.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @gabrielbryk/json-schema-to-zod
2
2
 
3
+ ## 2.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 719e761: Add `typeExports` option to export TypeScript types for all generated schemas
8
+
9
+ When `typeExports: true` is set (along with `exportRefs: true`), each generated schema will have a corresponding type export:
10
+
11
+ ```typescript
12
+ export const MySchema = z.object({...});
13
+ export type MySchema = z.infer<typeof MySchema>;
14
+ ```
15
+
16
+ This makes it easier to use the generated types throughout your codebase without manually creating type aliases.
17
+
18
+ ## 2.11.1
19
+
20
+ ### Patch Changes
21
+
22
+ - 466d672: Fix TS7056 error when generating declarations for schemas referencing recursive types
23
+
24
+ - Add explicit type annotations to any schema that references recursive schemas (not just unions)
25
+ - This prevents TypeScript from trying to serialize extremely large expanded types when generating .d.ts files
26
+ - Fix type narrowing in parseObject for allOf required array handling
27
+
3
28
  ## 2.11.0
4
29
 
5
30
  ### Minor Changes
package/check-types.sh CHANGED
@@ -8,7 +8,7 @@ import { readFileSync, writeFileSync } from 'fs';
8
8
  import { jsonSchemaToZod } from './src/jsonSchemaToZod.js';
9
9
 
10
10
  const schema = yaml.load(readFileSync('test/fixtures/workflow.yaml', 'utf8'));
11
- const output = jsonSchemaToZod(schema, { name: 'workflowSchema' });
11
+ const output = jsonSchemaToZod(schema, { name: 'workflowSchema', typeExports: true });
12
12
  writeFileSync('.tmp-workflow-schema-output.ts', output);
13
13
  console.log('Generated .tmp-workflow-schema-output.ts');
14
14
  EOF
@@ -149,7 +149,7 @@ const orderDeclarations = (entries, dependencies) => {
149
149
  };
150
150
  export const emitZod = (analysis) => {
151
151
  const { schema, options, refNameByPointer, cycleRefNames, cycleComponentByName, } = analysis;
152
- const { name, type, noImport, exportRefs, withMeta, ...rest } = options;
152
+ const { name, type, noImport, exportRefs, typeExports, withMeta, ...rest } = options;
153
153
  const declarations = new Map();
154
154
  const dependencies = new Map();
155
155
  // Fresh name registry for the emission pass.
@@ -196,14 +196,14 @@ export const emitZod = (analysis) => {
196
196
  : undefined;
197
197
  const hasLazy = expression.includes("z.lazy(");
198
198
  const hasGetter = expression.includes("get ");
199
- const isUnion = expression.startsWith("z.union(") || expression.startsWith("z.discriminatedUnion(");
200
- // Check if this union references any cycle members (recursive schemas)
201
- const referencesRecursiveSchema = isUnion && Array.from(cycleRefNames).some(cycleName => new RegExp(`\\b${cycleName}\\b`).test(expression));
199
+ // Check if this schema references any cycle members (recursive schemas)
200
+ // This can cause TS7056 when TypeScript tries to serialize the expanded type
201
+ const referencesRecursiveSchema = Array.from(cycleRefNames).some(cycleName => new RegExp(`\\b${cycleName}\\b`).test(expression));
202
202
  // Per Zod v4 docs: type annotations should be on GETTERS for recursive types, not on const declarations.
203
203
  // TypeScript can infer the type of const declarations.
204
204
  // Exceptions that need explicit type annotation:
205
205
  // 1. z.lazy() without getters
206
- // 2. Union types that reference recursive schemas (for proper type inference)
206
+ // 2. Any schema that references recursive schemas (to prevent TS7056)
207
207
  const needsTypeAnnotation = (hasLazy && !hasGetter) || referencesRecursiveSchema;
208
208
  const storedType = needsTypeAnnotation ? (hintedType ?? inferTypeFromExpression(expression)) : undefined;
209
209
  // Rule 2 from Zod v4: Don't chain methods on recursive types
@@ -227,6 +227,13 @@ export const emitZod = (analysis) => {
227
227
  expression: `${baseName}${methodChain}`,
228
228
  exported: exportRefs,
229
229
  });
230
+ // Export type for this declaration if typeExports is enabled
231
+ if (typeExports && exportRefs) {
232
+ emitter.addTypeExport({
233
+ name: refName,
234
+ type: `z.infer<typeof ${refName}>`,
235
+ });
236
+ }
230
237
  continue;
231
238
  }
232
239
  }
@@ -236,6 +243,13 @@ export const emitZod = (analysis) => {
236
243
  exported: exportRefs,
237
244
  typeAnnotation: storedType !== "z.ZodTypeAny" ? storedType : undefined,
238
245
  });
246
+ // Export type for this declaration if typeExports is enabled
247
+ if (typeExports && exportRefs) {
248
+ emitter.addTypeExport({
249
+ name: refName,
250
+ type: `z.infer<typeof ${refName}>`,
251
+ });
252
+ }
239
253
  }
240
254
  }
241
255
  if (name) {
@@ -252,7 +266,8 @@ export const emitZod = (analysis) => {
252
266
  jsdoc: jsdocs,
253
267
  });
254
268
  }
255
- if (type && name) {
269
+ // Export type for root schema if type option is set, or if typeExports is enabled
270
+ if (name && (type || typeExports)) {
256
271
  const typeName = typeof type === "string" ? type : `${name[0].toUpperCase()}${name.substring(1)}`;
257
272
  emitter.addTypeExport({
258
273
  name: typeName,
@@ -13,6 +13,16 @@ export function parseObject(objectSchema, refs) {
13
13
  const hasAdditionalProperties = objectSchema.additionalProperties !== undefined;
14
14
  const hasPatternProperties = objectSchema.patternProperties !== undefined;
15
15
  const hasNoDirectSchema = !hasDirectProperties && !hasAdditionalProperties && !hasPatternProperties;
16
+ const parentRequired = Array.isArray(objectSchema.required) ? objectSchema.required : [];
17
+ const allOfRequired = its.an.allOf(objectSchema)
18
+ ? objectSchema.allOf.flatMap((member) => {
19
+ if (typeof member !== "object" || member === null)
20
+ return [];
21
+ const req = member.required;
22
+ return Array.isArray(req) ? req : [];
23
+ })
24
+ : [];
25
+ const combinedAllOfRequired = [...new Set([...parentRequired, ...allOfRequired])];
16
26
  // Helper to add type: "object" to composition members that have properties but no explicit type
17
27
  const addObjectType = (members) => members.map((x) => typeof x === "object" &&
18
28
  x !== null &&
@@ -20,9 +30,31 @@ export function parseObject(objectSchema, refs) {
20
30
  (x.properties || x.additionalProperties || x.patternProperties)
21
31
  ? { ...x, type: "object" }
22
32
  : x);
33
+ const addObjectTypeAndMergeRequired = (members) => members.map((x) => {
34
+ if (typeof x !== "object" || x === null)
35
+ return x;
36
+ let normalized = x;
37
+ const hasShape = normalized.properties || normalized.additionalProperties || normalized.patternProperties;
38
+ if (hasShape && !normalized.type) {
39
+ normalized = { ...normalized, type: "object" };
40
+ }
41
+ if (combinedAllOfRequired.length &&
42
+ normalized.properties &&
43
+ Object.keys(normalized.properties).length) {
44
+ const memberRequired = Array.isArray(normalized.required) ? normalized.required : [];
45
+ const mergedRequired = Array.from(new Set([
46
+ ...memberRequired,
47
+ ...combinedAllOfRequired.filter((key) => Object.prototype.hasOwnProperty.call(normalized.properties, key)),
48
+ ]));
49
+ if (mergedRequired.length) {
50
+ normalized = { ...normalized, required: mergedRequired };
51
+ }
52
+ }
53
+ return normalized;
54
+ });
23
55
  // If only allOf, delegate to parseAllOf
24
56
  if (hasNoDirectSchema && its.an.allOf(objectSchema) && !its.an.anyOf(objectSchema) && !its.a.oneOf(objectSchema) && !its.a.conditional(objectSchema)) {
25
- return parseAllOf({ ...objectSchema, allOf: addObjectType(objectSchema.allOf) }, refs);
57
+ return parseAllOf({ ...objectSchema, allOf: addObjectTypeAndMergeRequired(objectSchema.allOf) }, refs);
26
58
  }
27
59
  // If only anyOf, delegate to parseAnyOf
28
60
  if (hasNoDirectSchema && its.an.anyOf(objectSchema) && !its.an.allOf(objectSchema) && !its.a.oneOf(objectSchema) && !its.a.conditional(objectSchema)) {
@@ -285,12 +317,7 @@ export function parseObject(objectSchema, refs) {
285
317
  if (its.an.allOf(objectSchema)) {
286
318
  const allOfResult = parseAllOf({
287
319
  ...objectSchema,
288
- allOf: objectSchema.allOf.map((x) => typeof x === "object" &&
289
- x !== null &&
290
- !x.type &&
291
- (x.properties || x.additionalProperties || x.patternProperties)
292
- ? { ...x, type: "object" }
293
- : x),
320
+ allOf: addObjectTypeAndMergeRequired(objectSchema.allOf),
294
321
  }, refs);
295
322
  output += `.and(${allOfResult.expression})`;
296
323
  intersectionTypes.push(allOfResult.type);
@@ -84,6 +84,18 @@ export type Options = {
84
84
  noImport?: boolean;
85
85
  /** Export all generated reference schemas (for $refs) when using ESM */
86
86
  exportRefs?: boolean;
87
+ /**
88
+ * Export TypeScript types for all generated schemas using z.infer.
89
+ * When true, exports a type for each schema (including lifted/ref schemas when exportRefs is true).
90
+ * Type names match their corresponding schema const names.
91
+ * @example
92
+ * // With typeExports: true, exportRefs: true, and name: "MySchema"
93
+ * export const SubSchema = z.object({...});
94
+ * export type SubSchema = z.infer<typeof SubSchema>;
95
+ * export const MySchema = z.object({ sub: SubSchema });
96
+ * export type MySchema = z.infer<typeof MySchema>;
97
+ */
98
+ typeExports?: boolean;
87
99
  /**
88
100
  * Store original JSON Schema constructs in .meta({ __jsonSchema: {...} })
89
101
  * for features that can't be natively represented in Zod (patternProperties,
@@ -0,0 +1,53 @@
1
+ # Proposal: Strengthen `allOf` required handling
2
+
3
+ ## Problem
4
+ - Required keys are dropped when properties live in a different `allOf` member than the `required` array (e.g., workflow schema `call`/`with`).
5
+ - Spread path only looks at each member’s own `required`, ignoring parent + sibling required-only members.
6
+ - Intersection fallback also skips parent required enforcement because `parseObject` disables missing-key checks when composition keywords exist.
7
+ - `$ref` members with required lists don’t inform optionality of sibling properties.
8
+ - Conflicting property definitions across `allOf` members fail silently (often ending up as permissive intersections).
9
+
10
+ ## Goals
11
+ 1) Preserve required semantics across `allOf`, even when properties and `required` are split across members.
12
+ 2) Keep spread optimization where safe; otherwise, enforce required keys in the intersection path.
13
+ 3) Respect `unevaluatedProperties`/`additionalProperties` constraints from stricter members.
14
+ 4) Surface conflicts clearly instead of silently widening to `z.any()`.
15
+
16
+ ## Proposed changes
17
+ - **Normalize `allOf` members up front (done partially):**
18
+ - Add `type: "object"` when shape hints exist and merge parent+member required into any member that actually declares those properties (already implemented for the spread path; extend to intersection path and `$ref` resolution).
19
+
20
+ - **Intersection required enforcement:**
21
+ - When spread is not possible, compute a combined required set (parent + all member `required` + required-only members) and add a `superRefine` that checks presence of those keys on the final intersection result.
22
+ - Skip keys that none of the members define (avoid false positives).
23
+
24
+ - **Required-only + properties-only pattern:**
25
+ - Detect an `allOf` where one member has only `required` and another has the properties; merge those required keys into the properties member before parsing.
26
+
27
+ - **$ref-aware required merge:**
28
+ - When an `allOf` member is a `$ref`, resolve its schema shape/required (using existing ref resolution) and merge required keys that match properties provided by other members.
29
+
30
+ - **Policy reconciliation:**
31
+ - When merging shapes, intersect `unevaluatedProperties`/`additionalProperties` so a member that disallows extras keeps that restriction after spread/intersection.
32
+
33
+ - **Conflict detection:**
34
+ - If multiple members define the same property with incompatible primitive types (e.g., `string` vs `number`), emit a `superRefine` that fails with a clear message instead of silently widening.
35
+
36
+ ## Testing
37
+ - Unit tests in `test/parsers/parseAllOf.test.ts` and `parseObject.test.ts` covering:
38
+ - properties-only + required-only split (current workflow pattern).
39
+ - Overlapping required across multiple members (including parent required).
40
+ - `$ref` member with required + sibling properties.
41
+ - Spread eligible vs. ineligible `allOf` and intersection fallback enforcing required.
42
+ - `unevaluatedProperties` interactions where one member disallows extras.
43
+ - Conflict detection on incompatible property types.
44
+
45
+ ## Risks & mitigations
46
+ - **Over-enforcement**: Ensure required keys are only checked when at least one member defines the property. Filter combined required sets accordingly.
47
+ - **Ref resolution cost**: Cache resolved `$ref` shapes when harvesting required sets to avoid repeated work.
48
+ - **False conflicts**: Limit conflict detection to clear primitive mismatches; avoid flagging unions/anyOf/any as conflicts.
49
+
50
+ ## Rollout
51
+ - Implement normalization + intersection required enforcement first, add tests for workflow fixture regression.
52
+ - Follow with `$ref`-aware required merge and policy reconciliation tests.
53
+ - Add conflict detection last (guarded behind a clear error message) to avoid unexpected breakage.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gabrielbryk/json-schema-to-zod",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Converts JSON schema objects or files into Zod schemas",
5
5
  "type": "module",
6
6
  "types": "./dist/types/index.d.ts",