Skip to content

Commit 09d7f1b

Browse files
the-ultclaude
andcommitted
fix(zod): address PR review + green CI for zod4 baseline
Resolve review feedback and unblock CI on #3642: - resolveIsZodV4: treat "packageJson present but no detectable zod version" the same as "no packageJson" and default to Zod 4, matching the documented 'auto' intent (Copilot). Add regression coverage. - normalizeOperationsAndTags: skip emitting a normalized zod object for operation/tag overrides that carry only unsupported (output-only) fields, so they are true no-ops and cannot inject default strict/generate/coerce values that override global override.zod.* during downstream merges (CodeRabbit). Add regression coverage. - types: convert empty `extends` interfaces (OperationZodOptions, NormalizedOperationZodOptions) to type aliases to satisfy the no-empty-object-type lint rule. - Run formatter on types.ts / options.test.ts (fixes the failing `vp fmt --check` CI step). - Regenerate 3 stale petstore-tags-split-zod-schemas snapshots to the Zod 4 baseline (zod.email() instead of zod.string().email()). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 70ea7a5 commit 09d7f1b

8 files changed

Lines changed: 93 additions & 18 deletions

File tree

packages/core/src/types.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,7 @@ export interface ZodOptions extends BaseZodOptions {
875875
* schema-layout settings belong on `override.zod`, not `override.operations.*`
876876
* / `override.tags.*`.
877877
*/
878-
export interface OperationZodOptions extends BaseZodOptions {}
878+
export type OperationZodOptions = BaseZodOptions;
879879

880880
export interface EffectOptions {
881881
strict?: ZodOptions['strict'];
@@ -934,16 +934,10 @@ export interface NormalizedZodOptions {
934934
timeOptions: ZodTimeOptions;
935935
}
936936

937-
export interface NormalizedOperationZodOptions
938-
extends Pick<
939-
NormalizedZodOptions,
940-
| 'strict'
941-
| 'generate'
942-
| 'coerce'
943-
| 'preprocess'
944-
| 'params'
945-
| 'useBrandedTypes'
946-
> {}
937+
export type NormalizedOperationZodOptions = Pick<
938+
NormalizedZodOptions,
939+
'strict' | 'generate' | 'coerce' | 'preprocess' | 'params' | 'useBrandedTypes'
940+
>;
947941

948942
export interface NormalizedEffectOptions {
949943
strict: NormalizedZodOptions['strict'];

packages/orval/src/utils/options.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1079,7 +1079,8 @@ describe('normalizeOptions', () => {
10791079
expect.stringContaining('override.tags.Pets.zod'),
10801080
);
10811081
expect(
1082-
'version' in (normalized.output.override.operations.listPets?.zod ?? {}),
1082+
'version' in
1083+
(normalized.output.override.operations.listPets?.zod ?? {}),
10831084
).toBe(false);
10841085
expect(
10851086
'generateMeta' in (normalized.output.override.tags.Pets?.zod ?? {}),
@@ -1090,6 +1091,57 @@ describe('normalizeOptions', () => {
10901091
}
10911092
});
10921093

1094+
it('emits no zod object for operation/tag overrides that only carry unsupported fields', async () => {
1095+
const workspace = await createTempWorkspace();
1096+
logWarningSpy.mockClear();
1097+
1098+
try {
1099+
const normalized = await normalizeOptions(
1100+
{
1101+
input: {
1102+
target: {
1103+
openapi: '3.1.0',
1104+
info: { title: 'Test', version: '1.0.0' },
1105+
paths: {},
1106+
},
1107+
},
1108+
output: {
1109+
target: './generated.ts',
1110+
client: 'zod',
1111+
override: {
1112+
operations: {
1113+
listPets: {
1114+
zod: {
1115+
version: 3,
1116+
} as never,
1117+
},
1118+
},
1119+
tags: {
1120+
Pets: {
1121+
zod: {
1122+
generateMeta: true,
1123+
} as never,
1124+
},
1125+
},
1126+
},
1127+
},
1128+
},
1129+
workspace,
1130+
);
1131+
1132+
// An unsupported-only entry must be a true no-op: no normalized zod
1133+
// object is emitted, so default strict/generate/coerce values can't leak
1134+
// in and override global `override.zod.*` during downstream merges.
1135+
expect(
1136+
normalized.output.override.operations.listPets?.zod,
1137+
).toBeUndefined();
1138+
expect(normalized.output.override.tags.Pets?.zod).toBeUndefined();
1139+
} finally {
1140+
await rm(workspace, { recursive: true, force: true });
1141+
logWarningSpy.mockClear();
1142+
}
1143+
});
1144+
10931145
it('resolves global query mutators relative to the output workspace', async () => {
10941146
const workspace = await createTempWorkspace();
10951147

packages/orval/src/utils/options.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,20 @@ function normalizeOperationsAndTags(
954954
);
955955
}
956956

957+
// Only emit a normalized zod object when the entry actually carries a
958+
// supported operation-level field. Otherwise an unsupported-only entry
959+
// (e.g. `{ version: 3 }`) would inject default strict/generate/coerce
960+
// values that override global `override.zod.*` during downstream merges,
961+
// contradicting the "ignored" warning above.
962+
const hasSupportedOperationZodConfig =
963+
!!zod &&
964+
(zod.strict !== undefined ||
965+
zod.generate !== undefined ||
966+
zod.coerce !== undefined ||
967+
zod.preprocess !== undefined ||
968+
zod.params !== undefined ||
969+
zod.useBrandedTypes !== undefined);
970+
957971
return [
958972
key,
959973
{
@@ -976,7 +990,7 @@ function normalizeOperationsAndTags(
976990
query: normalizeQueryOptions(query, workspace, global.query),
977991
}
978992
: {}),
979-
...(zod
993+
...(hasSupportedOperationZodConfig && zod
980994
? {
981995
zod: {
982996
strict: {

packages/zod/src/compatible-v4.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ describe('resolveIsZodV4', () => {
114114
it('defaults to v4 when no packageJson is available', () => {
115115
expect(resolveIsZodV4('auto', undefined)).toBe(true);
116116
});
117+
118+
it('defaults to v4 when packageJson has no detectable zod version', () => {
119+
expect(resolveIsZodV4('auto', {})).toBe(true);
120+
expect(resolveIsZodV4('auto', { dependencies: {} })).toBe(true);
121+
expect(
122+
resolveIsZodV4('auto', { dependencies: { 'other-pkg': '1.0.0' } }),
123+
).toBe(true);
124+
});
117125
});
118126

119127
describe('when version is pinned explicitly', () => {

packages/zod/src/compatible-v4.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,15 @@ export const resolveIsZodV4 = (
4848
}
4949

5050
// 'auto' (or unset) — infer from the installed/resolved zod version and
51-
// default to Zod 4 when the target workspace has no detectable package yet.
52-
return !packageJson || isZodVersionV4(packageJson);
51+
// default to Zod 4 when the target workspace has no detectable zod version
52+
// yet. Treat "no packageJson" and "packageJson without a detectable zod
53+
// version" identically so partially-installed workspaces still get the
54+
// modern baseline instead of silently falling back to Zod 3.
55+
if (!packageJson || !getZodPackageVersion(packageJson)) {
56+
return true;
57+
}
58+
59+
return isZodVersionV4(packageJson);
5360
};
5461

5562
export const getZodDateFormat = (isZodV4: boolean) => {

tests/__snapshots__/axios/petstore-tags-split-zod-schemas/model/pets/pet.zod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const Pet = zod
3636
id: zod.number(),
3737
name: zod.string(),
3838
tag: zod.string().optional(),
39-
email: zod.string().email().optional(),
39+
email: zod.email().optional(),
4040
callingCode: zod.enum(['+33', '+420']).optional(),
4141
country: zod.enum(["People's Republic of China", 'Uruguay']).optional(),
4242
}),

tests/__snapshots__/axios/petstore-tags-split-zod-schemas/model/pets/petWithTag.zod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const PetWithTag = zod.object({
3838
id: zod.number(),
3939
name: zod.string(),
4040
tag: zod.string().optional(),
41-
email: zod.string().email().optional(),
41+
email: zod.email().optional(),
4242
callingCode: zod.enum(['+33', '+420']).optional(),
4343
country: zod.enum(["People's Republic of China", 'Uruguay']).optional(),
4444
}),

tests/__snapshots__/axios/petstore-tags-split-zod-schemas/model/pets/pets.zod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const Pets = zod.array(
3737
id: zod.number(),
3838
name: zod.string(),
3939
tag: zod.string().optional(),
40-
email: zod.string().email().optional(),
40+
email: zod.email().optional(),
4141
callingCode: zod.enum(['+33', '+420']).optional(),
4242
country: zod.enum(["People's Republic of China", 'Uruguay']).optional(),
4343
}),

0 commit comments

Comments
 (0)