Skip to content

Commit 6f04836

Browse files
authored
fix(v4): derive JSON Schema constraints by folding checks in the converter (#6554)
* fix(v4): derive JSON Schema constraints by folding checks in the converter * fix(v4): keep integer conjunctions and mime narrowing order-independent in the fold * refactor(v4): stop checks writing metadata into the bag now the converter folds them * fix(v4): resolve lazy parts in template literals and keep derived getters assignable * test(v4): pin that a derived metadata member recomputes after deletion * refactor(v4): tidy the check fold Collapse the eight hand-written derived-member accessor pairs in classic into one `util.derived` installer; share the fold's narrowing, divisor, pattern, mime and format helpers between the contributors and the bag reconciliation; type the aggregate's bounds generically so the numeric consumers stop casting; have `partPattern` return a pattern source so the template literal reuses `cleanRegex`; flatten the file processor's mime branches. * fix(v4): keep derived members configurable, as the literal accessors they replace were * refactor(v4): split the template literal part fold from the wrapper substitution * refactor(v4): let checks declare a string's pattern on attach, and drop the template literal fold The pattern a string or number exposes for template literals comes from its checks again, the way it did before the bag strip, but declared structurally: length checks narrow the bag's bounds (tightest wins, whatever the chain order), pattern-carrying checks set the bag's pattern (last wins), and an integer format sets the integer pattern. The string and number inits read those three keys and nothing else derives part patterns, so wrappers and unions compose them as they always did and the template literal is back to its plain loop. The JSON Schema converter still folds the defs. `regexes.string` answers an empty range with a pattern that matches nothing instead of building an invalid quantifier. The mini ceilings go back to main's. * Revert "refactor(v4): let checks declare a string's pattern on attach, and drop the template literal fold" This reverts commit 4a5b175.
1 parent e4d67f3 commit 6f04836

12 files changed

Lines changed: 549 additions & 357 deletions

File tree

‎packages/treeshake/bundle-size.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,17 @@ const CEILINGS: Record<string, number> = {
4747
// Also carries the Standard Schema issue bag: a failing `~standard.validate` parses with a plain issue holder instead of constructing a ZodError. Measured 2974 / 3455 / 4540 locally plus 28 headroom.
4848
// Also carries the lazy `safeParse` error: a getter and a setter on the failing result, and the async wrapper that keeps a sync throw a rejection. Measured 3021 / 3497 / 4581 locally plus 18 headroom.
4949
// Also carries the symbol-key loop in `util.members`, which is what installs `$ZodProperties`'s `Symbol.iterator` on its prototype, and the memoizer's shared reference predicate. Measured 3036 / 3516 / 4611 locally plus 18 headroom.
50-
"zod-mini-string": 3534,
50+
// Lowered by the measured −105 when checks stopped writing metadata into the bag at attach time: the string length/format closures and the per-instance bounded regex left every string bundle, and the template literal now derives its own part patterns.
51+
"zod-mini-string": 3429,
5152
// Also carries the construction-time discriminator check, which writes a WeakMap entry from `$ZodObject`, so every bundle containing `z.object` pays for it whether or not it builds a discriminated union.
5253
// Also carries the declared symbol keys from #6448: `normalizeDef` collects the shape's own symbols and the parse loop walks them. Almost none of that is the `Reflect.ownKeys` conversions — reverting all eight of them measures a byte larger.
5354
// Also carries the memoizer seam from #6482: each container init reads `globalConfig.memoizer` and calls `attach`, which is what lets `zod/mini` opt into cycle support. Measured 4512 locally but 4533 on CI — the gzip stream differs across zlib builds — so the headroom rides on the CI number.
5455
// Also carries the Standard Schema issue bag: a failing `~standard.validate` parses with a plain issue holder instead of constructing a ZodError. Measured 2974 / 3455 / 4540 locally plus 28 headroom.
5556
// Also carries the lazy `safeParse` error: a getter and a setter on the failing result, and the async wrapper that keeps a sync throw a rejection. Measured 3021 / 3497 / 4581 locally plus 18 headroom.
5657
// Also carries the symbol-key loop in `util.members`, which is what installs `$ZodProperties`'s `Symbol.iterator` on its prototype, and the memoizer's shared reference predicate. Measured 3036 / 3516 / 4611 locally plus 18 headroom.
5758
// Also carries the guards `validate` parses under: an `aborted` check in each container loop, and two early exits in the generated object parser. Only a bundle containing a container pays — boolean is byte-identical to main and string measures three bytes smaller — so this ceiling rises by the measured +76 and the other two do not move. Measured 4696 locally plus 18 headroom.
58-
"zod-mini-object": 4714,
59+
// Lowered by the measured −69 for the same bag strip: the string and number inits no longer carry pattern derivation or attach-time metadata writes.
60+
"zod-mini-object": 4645,
5961
};
6062

6163
/**

‎packages/zod/src/v4/classic/schemas.ts‎

Lines changed: 180 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -391,59 +391,61 @@ export const _ZodString: core.$constructor<_ZodString> = /*@__PURE__*/ core.$con
391391
ZodType.init(inst, def);
392392

393393
inst._zod.processJSONSchema = (ctx, json, params) => processors.stringProcessor(inst, ctx, json, params);
394-
395-
const bag = inst._zod.bag;
396-
inst.format = bag.format ?? null;
397-
inst.minLength = bag.minimum ?? null;
398-
inst.maxLength = bag.maximum ?? null;
399394
},
400-
{
401-
regex(...args) {
402-
return this.check((checks.regex as any)(...args));
403-
},
404-
includes(...args) {
405-
return this.check((checks.includes as any)(...args));
406-
},
407-
startsWith(...args) {
408-
return this.check((checks.startsWith as any)(...args));
409-
},
410-
endsWith(...args) {
411-
return this.check((checks.endsWith as any)(...args));
412-
},
413-
min(...args) {
414-
return this.check((checks.minLength as any)(...args));
415-
},
416-
max(...args) {
417-
return this.check((checks.maxLength as any)(...args));
418-
},
419-
length(...args) {
420-
return this.check((checks.length as any)(...args));
421-
},
422-
nonempty(...args) {
423-
return this.check((checks.minLength as any)(1, ...args));
424-
},
425-
lowercase(params) {
426-
return this.check(checks.lowercase(params));
427-
},
428-
uppercase(params) {
429-
return this.check(checks.uppercase(params));
430-
},
431-
trim() {
432-
return this.check(checks.trim());
433-
},
434-
normalize(...args) {
435-
return this.check(checks.normalize(...args));
436-
},
437-
toLowerCase() {
438-
return this.check(checks.toLowerCase());
439-
},
440-
toUpperCase() {
441-
return this.check(checks.toUpperCase());
442-
},
443-
slugify() {
444-
return this.check(checks.slugify());
395+
/*@__PURE__*/ util.derived<_ZodString>(
396+
{
397+
format: (inst) => processors.aggregateChecks(inst).format ?? null,
398+
minLength: (inst) => processors.aggregateChecks<number>(inst).minimum ?? null,
399+
maxLength: (inst) => processors.aggregateChecks<number>(inst).maximum ?? null,
445400
},
446-
}
401+
{
402+
regex(...args) {
403+
return this.check((checks.regex as any)(...args));
404+
},
405+
includes(...args) {
406+
return this.check((checks.includes as any)(...args));
407+
},
408+
startsWith(...args) {
409+
return this.check((checks.startsWith as any)(...args));
410+
},
411+
endsWith(...args) {
412+
return this.check((checks.endsWith as any)(...args));
413+
},
414+
min(...args) {
415+
return this.check((checks.minLength as any)(...args));
416+
},
417+
max(...args) {
418+
return this.check((checks.maxLength as any)(...args));
419+
},
420+
length(...args) {
421+
return this.check((checks.length as any)(...args));
422+
},
423+
nonempty(...args) {
424+
return this.check((checks.minLength as any)(1, ...args));
425+
},
426+
lowercase(params) {
427+
return this.check(checks.lowercase(params));
428+
},
429+
uppercase(params) {
430+
return this.check(checks.uppercase(params));
431+
},
432+
trim() {
433+
return this.check(checks.trim());
434+
},
435+
normalize(...args) {
436+
return this.check(checks.normalize(...args));
437+
},
438+
toLowerCase() {
439+
return this.check(checks.toLowerCase());
440+
},
441+
toUpperCase() {
442+
return this.check(checks.toUpperCase());
443+
},
444+
slugify() {
445+
return this.check(checks.slugify());
446+
},
447+
}
448+
)
447449
);
448450

449451
export interface ZodString extends _ZodString<core.$ZodStringInternals<string>> {
@@ -1131,63 +1133,72 @@ export const ZodNumber: core.$constructor<ZodNumber> = /*@__PURE__*/ core.$const
11311133
ZodType.init(inst, def);
11321134

11331135
inst._zod.processJSONSchema = (ctx, json, params) => processors.numberProcessor(inst, ctx, json, params);
1134-
1135-
const bag = inst._zod.bag;
1136-
inst.minValue =
1137-
Math.max(bag.minimum ?? Number.NEGATIVE_INFINITY, bag.exclusiveMinimum ?? Number.NEGATIVE_INFINITY) ?? null;
1138-
inst.maxValue =
1139-
Math.min(bag.maximum ?? Number.POSITIVE_INFINITY, bag.exclusiveMaximum ?? Number.POSITIVE_INFINITY) ?? null;
1140-
inst.isInt = (bag.format ?? "").includes("int") || Number.isSafeInteger(bag.multipleOf ?? 0.5);
11411136
inst.isFinite = true;
1142-
inst.format = bag.format ?? null;
11431137
},
1144-
{
1145-
gt(value, params) {
1146-
return this.check(checks.gt(value, params));
1147-
},
1148-
gte(value, params) {
1149-
return this.check(checks.gte(value, params));
1150-
},
1151-
min(value, params) {
1152-
return this.check(checks.gte(value, params));
1153-
},
1154-
lt(value, params) {
1155-
return this.check(checks.lt(value, params));
1156-
},
1157-
lte(value, params) {
1158-
return this.check(checks.lte(value, params));
1159-
},
1160-
max(value, params) {
1161-
return this.check(checks.lte(value, params));
1162-
},
1163-
int(params) {
1164-
return this.check(int(params));
1165-
},
1166-
safe(params) {
1167-
return this.check(int(params));
1168-
},
1169-
positive(params) {
1170-
return this.check(checks.gt(0, params));
1171-
},
1172-
nonnegative(params) {
1173-
return this.check(checks.gte(0, params));
1174-
},
1175-
negative(params) {
1176-
return this.check(checks.lt(0, params));
1177-
},
1178-
nonpositive(params) {
1179-
return this.check(checks.lte(0, params));
1180-
},
1181-
multipleOf(value, params) {
1182-
return this.check(checks.multipleOf(value, params));
1183-
},
1184-
step(value, params) {
1185-
return this.check(checks.multipleOf(value, params));
1186-
},
1187-
finite() {
1188-
return this;
1138+
/*@__PURE__*/ util.derived<ZodNumber>(
1139+
{
1140+
minValue: (inst) => {
1141+
const { minimum, exclusiveMinimum } = processors.aggregateChecks<number>(inst);
1142+
return Math.max(minimum ?? Number.NEGATIVE_INFINITY, exclusiveMinimum ?? Number.NEGATIVE_INFINITY);
1143+
},
1144+
maxValue: (inst) => {
1145+
const { maximum, exclusiveMaximum } = processors.aggregateChecks<number>(inst);
1146+
return Math.min(maximum ?? Number.POSITIVE_INFINITY, exclusiveMaximum ?? Number.POSITIVE_INFINITY);
1147+
},
1148+
isInt: (inst) => {
1149+
const { isInt, multipleOf } = processors.aggregateChecks(inst);
1150+
return !!isInt || !!multipleOf?.some(Number.isSafeInteger);
1151+
},
1152+
format: (inst) => processors.aggregateChecks(inst).format ?? null,
11891153
},
1190-
}
1154+
{
1155+
gt(value, params) {
1156+
return this.check(checks.gt(value, params));
1157+
},
1158+
gte(value, params) {
1159+
return this.check(checks.gte(value, params));
1160+
},
1161+
min(value, params) {
1162+
return this.check(checks.gte(value, params));
1163+
},
1164+
lt(value, params) {
1165+
return this.check(checks.lt(value, params));
1166+
},
1167+
lte(value, params) {
1168+
return this.check(checks.lte(value, params));
1169+
},
1170+
max(value, params) {
1171+
return this.check(checks.lte(value, params));
1172+
},
1173+
int(params) {
1174+
return this.check(int(params));
1175+
},
1176+
safe(params) {
1177+
return this.check(int(params));
1178+
},
1179+
positive(params) {
1180+
return this.check(checks.gt(0, params));
1181+
},
1182+
nonnegative(params) {
1183+
return this.check(checks.gte(0, params));
1184+
},
1185+
negative(params) {
1186+
return this.check(checks.lt(0, params));
1187+
},
1188+
nonpositive(params) {
1189+
return this.check(checks.lte(0, params));
1190+
},
1191+
multipleOf(value, params) {
1192+
return this.check(checks.multipleOf(value, params));
1193+
},
1194+
step(value, params) {
1195+
return this.check(checks.multipleOf(value, params));
1196+
},
1197+
finite() {
1198+
return this;
1199+
},
1200+
}
1201+
)
11911202
);
11921203

11931204
export function number(params?: string | core.$ZodNumberParams): ZodNumber {
@@ -1277,47 +1288,49 @@ export const ZodBigInt: core.$constructor<ZodBigInt> = /*@__PURE__*/ core.$const
12771288
core.$ZodBigInt.init(inst, def);
12781289
ZodType.init(inst, def);
12791290
inst._zod.processJSONSchema = (ctx, json, params) => processors.bigintProcessor(inst, ctx, json, params);
1280-
1281-
const bag = inst._zod.bag;
1282-
inst.minValue = bag.minimum ?? null;
1283-
inst.maxValue = bag.maximum ?? null;
1284-
inst.format = bag.format ?? null;
12851291
},
1286-
{
1287-
gte(value, params) {
1288-
return this.check(checks.gte(value, params));
1289-
},
1290-
min(value, params) {
1291-
return this.check(checks.gte(value, params));
1292-
},
1293-
gt(value, params) {
1294-
return this.check(checks.gt(value, params));
1295-
},
1296-
lt(value, params) {
1297-
return this.check(checks.lt(value, params));
1298-
},
1299-
lte(value, params) {
1300-
return this.check(checks.lte(value, params));
1301-
},
1302-
max(value, params) {
1303-
return this.check(checks.lte(value, params));
1304-
},
1305-
positive(params) {
1306-
return this.check(checks.gt(BigInt(0), params));
1307-
},
1308-
negative(params) {
1309-
return this.check(checks.lt(BigInt(0), params));
1310-
},
1311-
nonpositive(params) {
1312-
return this.check(checks.lte(BigInt(0), params));
1313-
},
1314-
nonnegative(params) {
1315-
return this.check(checks.gte(BigInt(0), params));
1316-
},
1317-
multipleOf(value, params) {
1318-
return this.check(checks.multipleOf(value, params));
1292+
/*@__PURE__*/ util.derived<ZodBigInt>(
1293+
{
1294+
minValue: (inst) => processors.aggregateChecks<bigint>(inst).minimum ?? null,
1295+
maxValue: (inst) => processors.aggregateChecks<bigint>(inst).maximum ?? null,
1296+
format: (inst) => processors.aggregateChecks(inst).format ?? null,
13191297
},
1320-
}
1298+
{
1299+
gte(value, params) {
1300+
return this.check(checks.gte(value, params));
1301+
},
1302+
min(value, params) {
1303+
return this.check(checks.gte(value, params));
1304+
},
1305+
gt(value, params) {
1306+
return this.check(checks.gt(value, params));
1307+
},
1308+
lt(value, params) {
1309+
return this.check(checks.lt(value, params));
1310+
},
1311+
lte(value, params) {
1312+
return this.check(checks.lte(value, params));
1313+
},
1314+
max(value, params) {
1315+
return this.check(checks.lte(value, params));
1316+
},
1317+
positive(params) {
1318+
return this.check(checks.gt(BigInt(0), params));
1319+
},
1320+
negative(params) {
1321+
return this.check(checks.lt(BigInt(0), params));
1322+
},
1323+
nonpositive(params) {
1324+
return this.check(checks.lte(BigInt(0), params));
1325+
},
1326+
nonnegative(params) {
1327+
return this.check(checks.gte(BigInt(0), params));
1328+
},
1329+
multipleOf(value, params) {
1330+
return this.check(checks.multipleOf(value, params));
1331+
},
1332+
}
1333+
)
13211334
);
13221335

13231336
export function bigint(params?: string | core.$ZodBigIntParams): ZodBigInt {
@@ -1451,18 +1464,30 @@ export interface _ZodDate<T extends core.$ZodDateInternals = core.$ZodDateIntern
14511464
}
14521465

14531466
export interface ZodDate extends _ZodDate<core.$ZodDateInternals<Date>> {}
1454-
export const ZodDate: core.$constructor<ZodDate> = /*@__PURE__*/ core.$constructor("ZodDate", (inst, def) => {
1455-
core.$ZodDate.init(inst, def);
1456-
ZodType.init(inst, def);
1457-
inst._zod.processJSONSchema = (ctx, json, params) => processors.dateProcessor(inst, ctx, json, params);
1458-
1459-
inst.min = (value, params) => inst.check(checks.gte(value, params));
1460-
inst.max = (value, params) => inst.check(checks.lte(value, params));
1467+
export const ZodDate: core.$constructor<ZodDate> = /*@__PURE__*/ core.$constructor<ZodDate>(
1468+
"ZodDate",
1469+
(inst, def) => {
1470+
core.$ZodDate.init(inst, def);
1471+
ZodType.init(inst, def);
1472+
inst._zod.processJSONSchema = (ctx, json, params) => processors.dateProcessor(inst, ctx, json, params);
14611473

1462-
const c = inst._zod.bag;
1463-
inst.minDate = c.minimum ? new Date(c.minimum) : null;
1464-
inst.maxDate = c.maximum ? new Date(c.maximum) : null;
1465-
});
1474+
inst.min = (value, params) => inst.check(checks.gte(value, params));
1475+
inst.max = (value, params) => inst.check(checks.lte(value, params));
1476+
},
1477+
/*@__PURE__*/ util.derived<ZodDate>(
1478+
{
1479+
minDate: (inst) => {
1480+
const { minimum } = processors.aggregateChecks<Date>(inst);
1481+
return minimum ? new Date(minimum) : null;
1482+
},
1483+
maxDate: (inst) => {
1484+
const { maximum } = processors.aggregateChecks<Date>(inst);
1485+
return maximum ? new Date(maximum) : null;
1486+
},
1487+
},
1488+
{}
1489+
)
1490+
);
14661491

14671492
export function date(params?: string | core.$ZodDateParams): ZodDate {
14681493
return core._date(ZodDate, params);

0 commit comments

Comments
 (0)