zod 4.3.2 → 4.3.4
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/package.json +1 -1
- package/src/v4/classic/from-json-schema.ts +1 -1
- package/src/v4/classic/schemas.ts +1 -1
- package/src/v4/classic/tests/from-json-schema.test.ts +21 -0
- package/src/v4/classic/tests/record.test.ts +52 -0
- package/src/v4/classic/tests/to-json-schema.test.ts +111 -0
- package/src/v4/core/json-schema-processors.ts +34 -13
- package/src/v4/core/versions.ts +1 -1
- package/src/v4/mini/schemas.ts +0 -5
- package/v4/classic/from-json-schema.cjs +1 -1
- package/v4/classic/from-json-schema.js +1 -1
- package/v4/classic/schemas.d.cts +1 -1
- package/v4/classic/schemas.d.ts +1 -1
- package/v4/core/json-schema-processors.cjs +33 -13
- package/v4/core/json-schema-processors.js +33 -13
- package/v4/core/versions.cjs +1 -1
- package/v4/core/versions.js +1 -1
- package/v4/mini/schemas.cjs +0 -1
- package/v4/mini/schemas.d.cts +0 -1
- package/v4/mini/schemas.d.ts +0 -1
- package/v4/mini/schemas.js +0 -1
- package/src/v4/mini/tests/refine.test.ts +0 -19
package/package.json
CHANGED
|
@@ -597,7 +597,7 @@ function convertSchema(schema: JSONSchema.JSONSchema | boolean, ctx: ConversionC
|
|
|
597
597
|
}
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
-
// Content keywords
|
|
600
|
+
// Content keywords - store as metadata
|
|
601
601
|
const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
|
|
602
602
|
for (const key of contentMetadataKeys) {
|
|
603
603
|
if (key in schema) {
|
|
@@ -92,7 +92,7 @@ export interface ZodType<
|
|
|
92
92
|
refine<Ch extends (arg: core.output<this>) => unknown | Promise<unknown>>(
|
|
93
93
|
check: Ch,
|
|
94
94
|
params?: string | core.$ZodCustomParams
|
|
95
|
-
): Ch extends (arg: any) => arg is infer R ? core
|
|
95
|
+
): Ch extends (arg: any) => arg is infer R ? this & ZodType<R, core.input<this>> : this;
|
|
96
96
|
superRefine(
|
|
97
97
|
refinement: (arg: core.output<this>, ctx: core.$RefinementCtx<core.output<this>>) => void | Promise<void>
|
|
98
98
|
): this;
|
|
@@ -711,3 +711,24 @@ test("$comment and $anchor are captured as metadata", () => {
|
|
|
711
711
|
expect(meta?.$comment).toBe("This is a developer note");
|
|
712
712
|
expect(meta?.$anchor).toBe("my-anchor");
|
|
713
713
|
});
|
|
714
|
+
|
|
715
|
+
test("contentEncoding and contentMediaType are stored as metadata", () => {
|
|
716
|
+
const customRegistry = z.registry<{ contentEncoding?: string; contentMediaType?: string }>();
|
|
717
|
+
|
|
718
|
+
const schema = fromJSONSchema(
|
|
719
|
+
{
|
|
720
|
+
type: "string",
|
|
721
|
+
contentEncoding: "base64",
|
|
722
|
+
contentMediaType: "image/png",
|
|
723
|
+
},
|
|
724
|
+
{ registry: customRegistry }
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
// Should just be a string schema
|
|
728
|
+
expect(schema.parse("aGVsbG8gd29ybGQ=")).toBe("aGVsbG8gd29ybGQ=");
|
|
729
|
+
|
|
730
|
+
// Content keywords should be in metadata
|
|
731
|
+
const meta = customRegistry.get(schema);
|
|
732
|
+
expect(meta?.contentEncoding).toBe("base64");
|
|
733
|
+
expect(meta?.contentMediaType).toBe("image/png");
|
|
734
|
+
});
|
|
@@ -524,6 +524,58 @@ test("intersection of loose records", () => {
|
|
|
524
524
|
expect(() => schema.parse({ name: "John", N_count: "abc" })).toThrow(); // N_count should be number
|
|
525
525
|
});
|
|
526
526
|
|
|
527
|
+
test("object with looseRecord index signature", () => {
|
|
528
|
+
// Simulates TypeScript index signature: { label: string; [key: `label:${string}`]: string }
|
|
529
|
+
const schema = z.object({ label: z.string() }).and(z.looseRecord(z.string().regex(/^label:[a-z]{2}$/), z.string()));
|
|
530
|
+
|
|
531
|
+
type Schema = z.infer<typeof schema>;
|
|
532
|
+
expectTypeOf<Schema>().toEqualTypeOf<{ label: string } & Record<string, string>>();
|
|
533
|
+
|
|
534
|
+
// Valid: has required property and matching pattern keys
|
|
535
|
+
expect(schema.parse({ label: "Purple", "label:en": "Purple", "label:ru": "Пурпурный" })).toEqual({
|
|
536
|
+
label: "Purple",
|
|
537
|
+
"label:en": "Purple",
|
|
538
|
+
"label:ru": "Пурпурный",
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Valid: just required property
|
|
542
|
+
expect(schema.parse({ label: "Purple" })).toEqual({ label: "Purple" });
|
|
543
|
+
|
|
544
|
+
// Invalid: missing required property
|
|
545
|
+
expect(schema.safeParse({ "label:en": "Purple" })).toMatchInlineSnapshot(`
|
|
546
|
+
{
|
|
547
|
+
"error": [ZodError: [
|
|
548
|
+
{
|
|
549
|
+
"expected": "string",
|
|
550
|
+
"code": "invalid_type",
|
|
551
|
+
"path": [
|
|
552
|
+
"label"
|
|
553
|
+
],
|
|
554
|
+
"message": "Invalid input: expected string, received undefined"
|
|
555
|
+
}
|
|
556
|
+
]],
|
|
557
|
+
"success": false,
|
|
558
|
+
}
|
|
559
|
+
`);
|
|
560
|
+
|
|
561
|
+
// Invalid: pattern key with wrong value type
|
|
562
|
+
expect(schema.safeParse({ label: "Purple", "label:en": 123 })).toMatchInlineSnapshot(`
|
|
563
|
+
{
|
|
564
|
+
"error": [ZodError: [
|
|
565
|
+
{
|
|
566
|
+
"expected": "string",
|
|
567
|
+
"code": "invalid_type",
|
|
568
|
+
"path": [
|
|
569
|
+
"label:en"
|
|
570
|
+
],
|
|
571
|
+
"message": "Invalid input: expected string, received number"
|
|
572
|
+
}
|
|
573
|
+
]],
|
|
574
|
+
"success": false,
|
|
575
|
+
}
|
|
576
|
+
`);
|
|
577
|
+
});
|
|
578
|
+
|
|
527
579
|
test("numeric string keys", () => {
|
|
528
580
|
const schema = z.record(z.number(), z.number());
|
|
529
581
|
|
|
@@ -929,6 +929,117 @@ describe("toJSONSchema", () => {
|
|
|
929
929
|
`);
|
|
930
930
|
});
|
|
931
931
|
|
|
932
|
+
test("strict record with regex key uses propertyNames", () => {
|
|
933
|
+
const schema = z.record(z.string().regex(/^label:[a-z]{2}$/), z.string());
|
|
934
|
+
|
|
935
|
+
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
|
|
936
|
+
{
|
|
937
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
938
|
+
"additionalProperties": {
|
|
939
|
+
"type": "string",
|
|
940
|
+
},
|
|
941
|
+
"propertyNames": {
|
|
942
|
+
"pattern": "^label:[a-z]{2}$",
|
|
943
|
+
"type": "string",
|
|
944
|
+
},
|
|
945
|
+
"type": "object",
|
|
946
|
+
}
|
|
947
|
+
`);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
test("looseRecord with regex key uses patternProperties", () => {
|
|
951
|
+
const schema = z.looseRecord(z.string().regex(/^label:[a-z]{2}$/), z.string());
|
|
952
|
+
|
|
953
|
+
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
|
|
954
|
+
{
|
|
955
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
956
|
+
"patternProperties": {
|
|
957
|
+
"^label:[a-z]{2}$": {
|
|
958
|
+
"type": "string",
|
|
959
|
+
},
|
|
960
|
+
},
|
|
961
|
+
"type": "object",
|
|
962
|
+
}
|
|
963
|
+
`);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
test("looseRecord with multiple regex patterns uses patternProperties", () => {
|
|
967
|
+
const schema = z.looseRecord(
|
|
968
|
+
z
|
|
969
|
+
.string()
|
|
970
|
+
.regex(/^prefix_/)
|
|
971
|
+
.regex(/_suffix$/),
|
|
972
|
+
z.number()
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
|
|
976
|
+
{
|
|
977
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
978
|
+
"patternProperties": {
|
|
979
|
+
"^prefix_": {
|
|
980
|
+
"type": "number",
|
|
981
|
+
},
|
|
982
|
+
"_suffix$": {
|
|
983
|
+
"type": "number",
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
"type": "object",
|
|
987
|
+
}
|
|
988
|
+
`);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
test("looseRecord without regex key uses propertyNames", () => {
|
|
992
|
+
// looseRecord with plain string key should still use propertyNames
|
|
993
|
+
const schema = z.looseRecord(z.string(), z.boolean());
|
|
994
|
+
|
|
995
|
+
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
|
|
996
|
+
{
|
|
997
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
998
|
+
"additionalProperties": {
|
|
999
|
+
"type": "boolean",
|
|
1000
|
+
},
|
|
1001
|
+
"propertyNames": {
|
|
1002
|
+
"type": "string",
|
|
1003
|
+
},
|
|
1004
|
+
"type": "object",
|
|
1005
|
+
}
|
|
1006
|
+
`);
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
test("intersection of object with looseRecord uses patternProperties", () => {
|
|
1010
|
+
const zLabeled = z.object({ label: z.string() });
|
|
1011
|
+
const zLocalizedLabeled = z.looseRecord(z.string().regex(/^label:[a-z]{2}$/), z.string());
|
|
1012
|
+
const schema = zLabeled.and(zLocalizedLabeled);
|
|
1013
|
+
|
|
1014
|
+
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
|
|
1015
|
+
{
|
|
1016
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
1017
|
+
"allOf": [
|
|
1018
|
+
{
|
|
1019
|
+
"additionalProperties": false,
|
|
1020
|
+
"properties": {
|
|
1021
|
+
"label": {
|
|
1022
|
+
"type": "string",
|
|
1023
|
+
},
|
|
1024
|
+
},
|
|
1025
|
+
"required": [
|
|
1026
|
+
"label",
|
|
1027
|
+
],
|
|
1028
|
+
"type": "object",
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
"patternProperties": {
|
|
1032
|
+
"^label:[a-z]{2}$": {
|
|
1033
|
+
"type": "string",
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
1036
|
+
"type": "object",
|
|
1037
|
+
},
|
|
1038
|
+
],
|
|
1039
|
+
}
|
|
1040
|
+
`);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
932
1043
|
test("tuple", () => {
|
|
933
1044
|
const schema = z.tuple([z.string(), z.number()]);
|
|
934
1045
|
expect(z.toJSONSchema(schema)).toMatchInlineSnapshot(`
|
|
@@ -431,26 +431,47 @@ export const recordProcessor: Processor<schemas.$ZodRecord> = (schema, ctx, _jso
|
|
|
431
431
|
const json = _json as JSONSchema.ObjectSchema;
|
|
432
432
|
const def = schema._zod.def as schemas.$ZodRecordDef;
|
|
433
433
|
json.type = "object";
|
|
434
|
-
|
|
435
|
-
|
|
434
|
+
|
|
435
|
+
// For looseRecord with regex patterns, use patternProperties
|
|
436
|
+
// This correctly represents "only validate keys matching the pattern" semantics
|
|
437
|
+
// and composes well with allOf (intersections)
|
|
438
|
+
const keyType = def.keyType as schemas.$ZodTypes;
|
|
439
|
+
const keyBag = keyType._zod.bag as schemas.$ZodStringInternals<unknown>["bag"] | undefined;
|
|
440
|
+
const patterns = keyBag?.patterns;
|
|
441
|
+
|
|
442
|
+
if (def.mode === "loose" && patterns && patterns.size > 0) {
|
|
443
|
+
// Use patternProperties for looseRecord with regex patterns
|
|
444
|
+
const valueSchema = process(def.valueType, ctx as any, {
|
|
436
445
|
...params,
|
|
437
|
-
path: [...params.path, "
|
|
446
|
+
path: [...params.path, "patternProperties", "*"],
|
|
447
|
+
});
|
|
448
|
+
json.patternProperties = {};
|
|
449
|
+
for (const pattern of patterns) {
|
|
450
|
+
json.patternProperties[pattern.source] = valueSchema;
|
|
451
|
+
}
|
|
452
|
+
} else {
|
|
453
|
+
// Default behavior: use propertyNames + additionalProperties
|
|
454
|
+
if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
|
|
455
|
+
json.propertyNames = process(def.keyType, ctx as any, {
|
|
456
|
+
...params,
|
|
457
|
+
path: [...params.path, "propertyNames"],
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
json.additionalProperties = process(def.valueType, ctx as any, {
|
|
461
|
+
...params,
|
|
462
|
+
path: [...params.path, "additionalProperties"],
|
|
438
463
|
});
|
|
439
464
|
}
|
|
440
|
-
json.additionalProperties = process(def.valueType, ctx as any, {
|
|
441
|
-
...params,
|
|
442
|
-
path: [...params.path, "additionalProperties"],
|
|
443
|
-
});
|
|
444
465
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const
|
|
466
|
+
// Add required for keys with discrete values (enum, literal, etc.)
|
|
467
|
+
const keyValues = keyType._zod.values;
|
|
468
|
+
if (keyValues) {
|
|
469
|
+
const validKeyValues = [...keyValues].filter(
|
|
449
470
|
(v): v is string | number => typeof v === "string" || typeof v === "number"
|
|
450
471
|
);
|
|
451
472
|
|
|
452
|
-
if (
|
|
453
|
-
json.required =
|
|
473
|
+
if (validKeyValues.length > 0) {
|
|
474
|
+
json.required = validKeyValues as string[];
|
|
454
475
|
}
|
|
455
476
|
}
|
|
456
477
|
};
|
package/src/v4/core/versions.ts
CHANGED
package/src/v4/mini/schemas.ts
CHANGED
|
@@ -12,10 +12,6 @@ export interface ZodMiniType<
|
|
|
12
12
|
type: Internals["def"]["type"];
|
|
13
13
|
check(...checks: (core.CheckFn<core.output<this>> | core.$ZodCheck<core.output<this>>)[]): this;
|
|
14
14
|
with(...checks: (core.CheckFn<core.output<this>> | core.$ZodCheck<core.output<this>>)[]): this;
|
|
15
|
-
refine<Ch extends (arg: core.output<this>) => unknown | Promise<unknown>>(
|
|
16
|
-
check: Ch,
|
|
17
|
-
params?: string | core.$ZodCustomParams
|
|
18
|
-
): Ch extends (arg: any) => arg is infer R ? core.$ZodNarrow<this, R> : this;
|
|
19
15
|
clone(def?: Internals["def"], params?: { parent: boolean }): this;
|
|
20
16
|
register<R extends core.$ZodRegistry>(
|
|
21
17
|
registry: R,
|
|
@@ -72,7 +68,6 @@ export const ZodMiniType: core.$constructor<ZodMiniType> = /*@__PURE__*/ core.$c
|
|
|
72
68
|
);
|
|
73
69
|
};
|
|
74
70
|
inst.with = inst.check;
|
|
75
|
-
inst.refine = (check, params) => inst.check(refine(check, params)) as never;
|
|
76
71
|
inst.clone = (_def, params) => core.clone(inst, _def, params);
|
|
77
72
|
inst.brand = () => inst as any;
|
|
78
73
|
inst.register = ((reg: any, meta: any) => {
|
|
@@ -571,7 +571,7 @@ function convertSchema(schema, ctx) {
|
|
|
571
571
|
extraMeta[key] = schema[key];
|
|
572
572
|
}
|
|
573
573
|
}
|
|
574
|
-
// Content keywords
|
|
574
|
+
// Content keywords - store as metadata
|
|
575
575
|
const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
|
|
576
576
|
for (const key of contentMetadataKeys) {
|
|
577
577
|
if (key in schema) {
|
|
@@ -545,7 +545,7 @@ function convertSchema(schema, ctx) {
|
|
|
545
545
|
extraMeta[key] = schema[key];
|
|
546
546
|
}
|
|
547
547
|
}
|
|
548
|
-
// Content keywords
|
|
548
|
+
// Content keywords - store as metadata
|
|
549
549
|
const contentMetadataKeys = ["contentEncoding", "contentMediaType", "contentSchema"];
|
|
550
550
|
for (const key of contentMetadataKeys) {
|
|
551
551
|
if (key in schema) {
|
package/v4/classic/schemas.d.cts
CHANGED
|
@@ -35,7 +35,7 @@ export interface ZodType<out Output = unknown, out Input = unknown, out Internal
|
|
|
35
35
|
safeDecode(data: core.input<this>, params?: core.ParseContext<core.$ZodIssue>): parse.ZodSafeParseResult<core.output<this>>;
|
|
36
36
|
safeEncodeAsync(data: core.output<this>, params?: core.ParseContext<core.$ZodIssue>): Promise<parse.ZodSafeParseResult<core.input<this>>>;
|
|
37
37
|
safeDecodeAsync(data: core.input<this>, params?: core.ParseContext<core.$ZodIssue>): Promise<parse.ZodSafeParseResult<core.output<this>>>;
|
|
38
|
-
refine<Ch extends (arg: core.output<this>) => unknown | Promise<unknown>>(check: Ch, params?: string | core.$ZodCustomParams): Ch extends (arg: any) => arg is infer R ? core
|
|
38
|
+
refine<Ch extends (arg: core.output<this>) => unknown | Promise<unknown>>(check: Ch, params?: string | core.$ZodCustomParams): Ch extends (arg: any) => arg is infer R ? this & ZodType<R, core.input<this>> : this;
|
|
39
39
|
superRefine(refinement: (arg: core.output<this>, ctx: core.$RefinementCtx<core.output<this>>) => void | Promise<void>): this;
|
|
40
40
|
overwrite(fn: (x: core.output<this>) => core.output<this>): this;
|
|
41
41
|
optional(): ZodOptional<this>;
|
package/v4/classic/schemas.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface ZodType<out Output = unknown, out Input = unknown, out Internal
|
|
|
35
35
|
safeDecode(data: core.input<this>, params?: core.ParseContext<core.$ZodIssue>): parse.ZodSafeParseResult<core.output<this>>;
|
|
36
36
|
safeEncodeAsync(data: core.output<this>, params?: core.ParseContext<core.$ZodIssue>): Promise<parse.ZodSafeParseResult<core.input<this>>>;
|
|
37
37
|
safeDecodeAsync(data: core.input<this>, params?: core.ParseContext<core.$ZodIssue>): Promise<parse.ZodSafeParseResult<core.output<this>>>;
|
|
38
|
-
refine<Ch extends (arg: core.output<this>) => unknown | Promise<unknown>>(check: Ch, params?: string | core.$ZodCustomParams): Ch extends (arg: any) => arg is infer R ? core
|
|
38
|
+
refine<Ch extends (arg: core.output<this>) => unknown | Promise<unknown>>(check: Ch, params?: string | core.$ZodCustomParams): Ch extends (arg: any) => arg is infer R ? this & ZodType<R, core.input<this>> : this;
|
|
39
39
|
superRefine(refinement: (arg: core.output<this>, ctx: core.$RefinementCtx<core.output<this>>) => void | Promise<void>): this;
|
|
40
40
|
overwrite(fn: (x: core.output<this>) => core.output<this>): this;
|
|
41
41
|
optional(): ZodOptional<this>;
|
|
@@ -436,22 +436,42 @@ const recordProcessor = (schema, ctx, _json, params) => {
|
|
|
436
436
|
const json = _json;
|
|
437
437
|
const def = schema._zod.def;
|
|
438
438
|
json.type = "object";
|
|
439
|
-
|
|
440
|
-
|
|
439
|
+
// For looseRecord with regex patterns, use patternProperties
|
|
440
|
+
// This correctly represents "only validate keys matching the pattern" semantics
|
|
441
|
+
// and composes well with allOf (intersections)
|
|
442
|
+
const keyType = def.keyType;
|
|
443
|
+
const keyBag = keyType._zod.bag;
|
|
444
|
+
const patterns = keyBag?.patterns;
|
|
445
|
+
if (def.mode === "loose" && patterns && patterns.size > 0) {
|
|
446
|
+
// Use patternProperties for looseRecord with regex patterns
|
|
447
|
+
const valueSchema = (0, to_json_schema_js_1.process)(def.valueType, ctx, {
|
|
441
448
|
...params,
|
|
442
|
-
path: [...params.path, "
|
|
449
|
+
path: [...params.path, "patternProperties", "*"],
|
|
443
450
|
});
|
|
451
|
+
json.patternProperties = {};
|
|
452
|
+
for (const pattern of patterns) {
|
|
453
|
+
json.patternProperties[pattern.source] = valueSchema;
|
|
454
|
+
}
|
|
444
455
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
456
|
+
else {
|
|
457
|
+
// Default behavior: use propertyNames + additionalProperties
|
|
458
|
+
if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
|
|
459
|
+
json.propertyNames = (0, to_json_schema_js_1.process)(def.keyType, ctx, {
|
|
460
|
+
...params,
|
|
461
|
+
path: [...params.path, "propertyNames"],
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
json.additionalProperties = (0, to_json_schema_js_1.process)(def.valueType, ctx, {
|
|
465
|
+
...params,
|
|
466
|
+
path: [...params.path, "additionalProperties"],
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
// Add required for keys with discrete values (enum, literal, etc.)
|
|
470
|
+
const keyValues = keyType._zod.values;
|
|
471
|
+
if (keyValues) {
|
|
472
|
+
const validKeyValues = [...keyValues].filter((v) => typeof v === "string" || typeof v === "number");
|
|
473
|
+
if (validKeyValues.length > 0) {
|
|
474
|
+
json.required = validKeyValues;
|
|
455
475
|
}
|
|
456
476
|
}
|
|
457
477
|
};
|
|
@@ -404,22 +404,42 @@ export const recordProcessor = (schema, ctx, _json, params) => {
|
|
|
404
404
|
const json = _json;
|
|
405
405
|
const def = schema._zod.def;
|
|
406
406
|
json.type = "object";
|
|
407
|
-
|
|
408
|
-
|
|
407
|
+
// For looseRecord with regex patterns, use patternProperties
|
|
408
|
+
// This correctly represents "only validate keys matching the pattern" semantics
|
|
409
|
+
// and composes well with allOf (intersections)
|
|
410
|
+
const keyType = def.keyType;
|
|
411
|
+
const keyBag = keyType._zod.bag;
|
|
412
|
+
const patterns = keyBag?.patterns;
|
|
413
|
+
if (def.mode === "loose" && patterns && patterns.size > 0) {
|
|
414
|
+
// Use patternProperties for looseRecord with regex patterns
|
|
415
|
+
const valueSchema = process(def.valueType, ctx, {
|
|
409
416
|
...params,
|
|
410
|
-
path: [...params.path, "
|
|
417
|
+
path: [...params.path, "patternProperties", "*"],
|
|
411
418
|
});
|
|
419
|
+
json.patternProperties = {};
|
|
420
|
+
for (const pattern of patterns) {
|
|
421
|
+
json.patternProperties[pattern.source] = valueSchema;
|
|
422
|
+
}
|
|
412
423
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
424
|
+
else {
|
|
425
|
+
// Default behavior: use propertyNames + additionalProperties
|
|
426
|
+
if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") {
|
|
427
|
+
json.propertyNames = process(def.keyType, ctx, {
|
|
428
|
+
...params,
|
|
429
|
+
path: [...params.path, "propertyNames"],
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
json.additionalProperties = process(def.valueType, ctx, {
|
|
433
|
+
...params,
|
|
434
|
+
path: [...params.path, "additionalProperties"],
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
// Add required for keys with discrete values (enum, literal, etc.)
|
|
438
|
+
const keyValues = keyType._zod.values;
|
|
439
|
+
if (keyValues) {
|
|
440
|
+
const validKeyValues = [...keyValues].filter((v) => typeof v === "string" || typeof v === "number");
|
|
441
|
+
if (validKeyValues.length > 0) {
|
|
442
|
+
json.required = validKeyValues;
|
|
423
443
|
}
|
|
424
444
|
}
|
|
425
445
|
};
|
package/v4/core/versions.cjs
CHANGED
package/v4/core/versions.js
CHANGED
package/v4/mini/schemas.cjs
CHANGED
|
@@ -149,7 +149,6 @@ exports.ZodMiniType = core.$constructor("ZodMiniType", (inst, def) => {
|
|
|
149
149
|
}, { parent: true });
|
|
150
150
|
};
|
|
151
151
|
inst.with = inst.check;
|
|
152
|
-
inst.refine = (check, params) => inst.check(refine(check, params));
|
|
153
152
|
inst.clone = (_def, params) => core.clone(inst, _def, params);
|
|
154
153
|
inst.brand = () => inst;
|
|
155
154
|
inst.register = ((reg, meta) => {
|
package/v4/mini/schemas.d.cts
CHANGED
|
@@ -5,7 +5,6 @@ export interface ZodMiniType<out Output = unknown, out Input = unknown, out Inte
|
|
|
5
5
|
type: Internals["def"]["type"];
|
|
6
6
|
check(...checks: (core.CheckFn<core.output<this>> | core.$ZodCheck<core.output<this>>)[]): this;
|
|
7
7
|
with(...checks: (core.CheckFn<core.output<this>> | core.$ZodCheck<core.output<this>>)[]): this;
|
|
8
|
-
refine<Ch extends (arg: core.output<this>) => unknown | Promise<unknown>>(check: Ch, params?: string | core.$ZodCustomParams): Ch extends (arg: any) => arg is infer R ? core.$ZodNarrow<this, R> : this;
|
|
9
8
|
clone(def?: Internals["def"], params?: {
|
|
10
9
|
parent: boolean;
|
|
11
10
|
}): this;
|
package/v4/mini/schemas.d.ts
CHANGED
|
@@ -5,7 +5,6 @@ export interface ZodMiniType<out Output = unknown, out Input = unknown, out Inte
|
|
|
5
5
|
type: Internals["def"]["type"];
|
|
6
6
|
check(...checks: (core.CheckFn<core.output<this>> | core.$ZodCheck<core.output<this>>)[]): this;
|
|
7
7
|
with(...checks: (core.CheckFn<core.output<this>> | core.$ZodCheck<core.output<this>>)[]): this;
|
|
8
|
-
refine<Ch extends (arg: core.output<this>) => unknown | Promise<unknown>>(check: Ch, params?: string | core.$ZodCustomParams): Ch extends (arg: any) => arg is infer R ? core.$ZodNarrow<this, R> : this;
|
|
9
8
|
clone(def?: Internals["def"], params?: {
|
|
10
9
|
parent: boolean;
|
|
11
10
|
}): this;
|
package/v4/mini/schemas.js
CHANGED
|
@@ -21,7 +21,6 @@ export const ZodMiniType = /*@__PURE__*/ core.$constructor("ZodMiniType", (inst,
|
|
|
21
21
|
}, { parent: true });
|
|
22
22
|
};
|
|
23
23
|
inst.with = inst.check;
|
|
24
|
-
inst.refine = (check, params) => inst.check(refine(check, params));
|
|
25
24
|
inst.clone = (_def, params) => core.clone(inst, _def, params);
|
|
26
25
|
inst.brand = () => inst;
|
|
27
26
|
inst.register = ((reg, meta) => {
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { describe, expectTypeOf, test } from "vitest";
|
|
2
|
-
import type * as core from "../../core/index.js";
|
|
3
|
-
import * as z from "../index.js";
|
|
4
|
-
|
|
5
|
-
describe("type refinement with type guards", () => {
|
|
6
|
-
test("type guard narrows output type", () => {
|
|
7
|
-
const schema = z.string().refine((s): s is "a" => s === "a");
|
|
8
|
-
|
|
9
|
-
expectTypeOf<core.input<typeof schema>>().toEqualTypeOf<string>();
|
|
10
|
-
expectTypeOf<core.output<typeof schema>>().toEqualTypeOf<"a">();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
test("non-type-guard refine does not narrow", () => {
|
|
14
|
-
const schema = z.string().refine((s) => s.length > 0);
|
|
15
|
-
|
|
16
|
-
expectTypeOf<core.input<typeof schema>>().toEqualTypeOf<string>();
|
|
17
|
-
expectTypeOf<core.output<typeof schema>>().toEqualTypeOf<string>();
|
|
18
|
-
});
|
|
19
|
-
});
|