@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 +25 -0
- package/check-types.sh +1 -1
- package/dist/core/emitZod.js +21 -6
- package/dist/parsers/parseObject.js +34 -7
- package/dist/types/Types.d.ts +12 -0
- package/docs/proposals/allof-required-merging.md +53 -0
- package/package.json +1 -1
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
|
package/dist/core/emitZod.js
CHANGED
|
@@ -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
|
-
|
|
200
|
-
//
|
|
201
|
-
const referencesRecursiveSchema =
|
|
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.
|
|
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
|
|
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:
|
|
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
|
|
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);
|
package/dist/types/Types.d.ts
CHANGED
|
@@ -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.
|