@gabrielbryk/json-schema-to-zod 2.12.0 → 2.12.1
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 +6 -0
- package/dist/parsers/parseObject.js +73 -21
- package/dist/parsers/parseOneOf.js +10 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @gabrielbryk/json-schema-to-zod
|
|
2
2
|
|
|
3
|
+
## 2.12.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- e8cbafc: Fix `unevaluatedProperties: false` with `oneOf` by avoiding strict union branches, allowing base properties through, and enforcing unknown-key rejection after composition.
|
|
8
|
+
|
|
3
9
|
## 2.12.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
|
@@ -6,6 +6,25 @@ import { parseIfThenElse } from "./parseIfThenElse.js";
|
|
|
6
6
|
import { addJsdocs } from "../utils/jsdocs.js";
|
|
7
7
|
import { anyOrUnknown } from "../utils/anyOrUnknown.js";
|
|
8
8
|
import { containsRecursiveRef, inferTypeFromExpression } from "../utils/schemaRepresentation.js";
|
|
9
|
+
const collectKnownPropertyKeys = (schema) => {
|
|
10
|
+
const keys = new Set();
|
|
11
|
+
const visit = (node) => {
|
|
12
|
+
if (typeof node !== "object" || node === null)
|
|
13
|
+
return;
|
|
14
|
+
const obj = node;
|
|
15
|
+
if (obj.properties && typeof obj.properties === "object") {
|
|
16
|
+
Object.keys(obj.properties).forEach((key) => keys.add(key));
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
visit(schema);
|
|
20
|
+
if (Array.isArray(schema.oneOf))
|
|
21
|
+
schema.oneOf.forEach(visit);
|
|
22
|
+
if (Array.isArray(schema.anyOf))
|
|
23
|
+
schema.anyOf.forEach(visit);
|
|
24
|
+
if (Array.isArray(schema.allOf))
|
|
25
|
+
schema.allOf.forEach(visit);
|
|
26
|
+
return Array.from(keys);
|
|
27
|
+
};
|
|
9
28
|
export function parseObject(objectSchema, refs) {
|
|
10
29
|
// Optimization: if we have composition keywords (allOf/anyOf/oneOf) but no direct properties,
|
|
11
30
|
// delegate entirely to the composition parser to avoid generating z.object({}).and(...)
|
|
@@ -230,28 +249,40 @@ export function parseObject(objectSchema, refs) {
|
|
|
230
249
|
// we should NOT default to z.record(z.string(), z.any()) because that would allow any properties.
|
|
231
250
|
// Instead, use z.object({}) and let the .and() call add properties from the composition.
|
|
232
251
|
// This is especially important when unevaluatedProperties: false is set.
|
|
252
|
+
const shouldPassthroughForUnevaluated = unevaluated === false && hasCompositionKeywords;
|
|
253
|
+
const passthroughProperties = shouldPassthroughForUnevaluated && properties && !patternProperties
|
|
254
|
+
? `${properties}.passthrough()`
|
|
255
|
+
: properties;
|
|
233
256
|
const fallback = anyOrUnknown(refs);
|
|
234
|
-
let output
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
257
|
+
let output;
|
|
258
|
+
if (properties) {
|
|
259
|
+
if (patternProperties) {
|
|
260
|
+
output = properties + patternProperties;
|
|
261
|
+
}
|
|
262
|
+
else if (additionalProperties) {
|
|
263
|
+
if (additionalProperties.expression === "z.never()") {
|
|
264
|
+
// Don't use .strict() if there are composition keywords that add properties
|
|
265
|
+
output = hasCompositionKeywords ? passthroughProperties : properties + ".strict()";
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
output = properties + `.catchall(${additionalProperties.expression})`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
output = passthroughProperties;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else if (patternProperties) {
|
|
276
|
+
output = patternProperties;
|
|
277
|
+
}
|
|
278
|
+
else if (additionalProperties) {
|
|
279
|
+
output = `z.record(z.string(), ${additionalProperties.expression})`;
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
// If we have composition keywords, start with empty object instead of z.record()
|
|
283
|
+
// The composition will provide the actual schema via .and()
|
|
284
|
+
output = hasCompositionKeywords ? "z.object({})" : `z.record(z.string(), ${fallback.expression})`;
|
|
285
|
+
}
|
|
255
286
|
if (unevaluated === false && properties && !hasCompositionKeywords) {
|
|
256
287
|
output += ".strict()";
|
|
257
288
|
}
|
|
@@ -328,6 +359,27 @@ export function parseObject(objectSchema, refs) {
|
|
|
328
359
|
output += `.and(${conditionalResult.expression})`;
|
|
329
360
|
intersectionTypes.push(conditionalResult.type);
|
|
330
361
|
}
|
|
362
|
+
if (unevaluated === false && hasCompositionKeywords) {
|
|
363
|
+
const knownKeys = collectKnownPropertyKeys(objectSchema);
|
|
364
|
+
const patternRegexps = objectSchema.patternProperties
|
|
365
|
+
? Object.keys(objectSchema.patternProperties).map((pattern) => new RegExp(pattern))
|
|
366
|
+
: [];
|
|
367
|
+
const patternRegexpsLiteral = patternRegexps.length
|
|
368
|
+
? `[${patternRegexps.map((r) => r.toString()).join(", ")}]`
|
|
369
|
+
: "[new RegExp(\"$^\")]";
|
|
370
|
+
output += `.superRefine((value, ctx) => {
|
|
371
|
+
if (!value || typeof value !== "object") return;
|
|
372
|
+
const knownKeys = ${JSON.stringify(knownKeys)};
|
|
373
|
+
const patternRegexps = ${patternRegexpsLiteral};
|
|
374
|
+
for (const key in value) {
|
|
375
|
+
const isKnown = knownKeys.includes(key);
|
|
376
|
+
const matchesPattern = patternRegexps.length ? patternRegexps.some((r) => r.test(key)) : false;
|
|
377
|
+
if (!isKnown && !matchesPattern) {
|
|
378
|
+
ctx.addIssue({ code: "unrecognized_keys", keys: [key], path: [key], message: "Unknown property" });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
})`;
|
|
382
|
+
}
|
|
331
383
|
// Only add required validation for missing keys when there are no composition keywords
|
|
332
384
|
// When allOf/anyOf/oneOf exist, they should define the properties and handle required validation
|
|
333
385
|
if (missingRequiredKeys.length > 0 && !hasCompositionKeywords) {
|
|
@@ -297,6 +297,14 @@ export const parseOneOf = (schema, refs) => {
|
|
|
297
297
|
// in Zod v4 because ZodDiscriminatedUnion cannot be nested inside ZodUnion at the type level.
|
|
298
298
|
// The runtime would work, but the types wouldn't match, causing compile errors.
|
|
299
299
|
// So we fall through to the regular union handling below.
|
|
300
|
+
// If the parent object has its own shape (properties/patternProperties/additionalProperties)
|
|
301
|
+
// or explicitly forbids unevaluated properties, we shouldn't make the oneOf branches strict.
|
|
302
|
+
// Otherwise, the intersection with the parent schema would reject the parent's properties
|
|
303
|
+
// before the union gets a chance to validate.
|
|
304
|
+
const parentHasDirectObjectShape = Boolean(schema.properties ||
|
|
305
|
+
schema.patternProperties ||
|
|
306
|
+
schema.additionalProperties);
|
|
307
|
+
const parentForbidsUnevaluated = schema.unevaluatedProperties === false;
|
|
300
308
|
// Fallback: Standard z.union
|
|
301
309
|
const parsedSchemas = schema.oneOf.map((s, i) => {
|
|
302
310
|
const extracted = extractInlineObject(s, refs, [...refs.path, "oneOf", i]);
|
|
@@ -317,6 +325,8 @@ export const parseOneOf = (schema, refs) => {
|
|
|
317
325
|
parsed.expression.startsWith("z.object(") && // Critical check: Must be a Zod object
|
|
318
326
|
!parsed.expression.includes(".and(") &&
|
|
319
327
|
!parsed.expression.includes(".intersection(") &&
|
|
328
|
+
!parentHasDirectObjectShape &&
|
|
329
|
+
!parentForbidsUnevaluated &&
|
|
320
330
|
!parsed.expression.includes(".strict()") &&
|
|
321
331
|
!parsed.expression.includes(".catchall") &&
|
|
322
332
|
!parsed.expression.includes(".passthrough")) {
|