Skip to content

Commit 6a67a50

Browse files
the-ultThe UltmellowareCopilot
authored
fix(zod): fix split-mode schema generation output (#2964)
* fix(zod): fix split-mode schema generation output - emit Zod const constraints before schema exports in split files\n- group case-insensitive schema file paths to prevent write collisions\n- generate loose generic object schemas for v4 (and passthrough for v3)\n- add regressions for @type keys, generic objects, and split writer merges\n\nRefs #2933 * fix(zod): prevent trailing enum chain on array schemas and add regression tests Skip .enum() emission when schema type is 'array' to prevent duplicate enum chains caused by dereference propagating items.enum to the parent. Add regression tests for: - #2765: array-of-enum trailing .enum() (direct + dereferenced) - #2801: default const ordering in split output Closes #2765 Refs #2933, #2947, #2793, #2801 * fix(zod): fix split-mode schema generation output - emit Zod const constraints before schema exports in split files\n- group case-insensitive schema file paths to prevent write collisions\n- generate loose generic object schemas for v4 (and passthrough for v3)\n- add regressions for @type keys, generic objects, and split writer merges\n\nRefs #2933 * fix(zod): prevent trailing enum chain on array schemas and add regression tests Skip .enum() emission when schema type is 'array' to prevent duplicate enum chains caused by dereference propagating items.enum to the parent. Add regression tests for: - #2765: array-of-enum trailing .enum() (direct + dereferenced) - #2801: default const ordering in split output Closes #2765 Refs #2933, #2947, #2793, #2801 * Update packages/orval/src/write-zod-specs.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Refactor sorting to use toSorted method * Fix redundant sorting in write-zod-specs.ts * fix(zod): finalize split-mode schema generation regressions add zod v3/v4 compatibility coverage and split-writer regressions tighten zod schema parsing/type handling and refresh generated sample output * fix(zod): preserve ref sibling fields and format args Fixes split-mode review follow-ups:\n- format array function args as comma-separated params (e.g. stringFormat)\n- preserve sibling fields (nullable/description/etc.) during dereference\n- restore nullable in generated PetWithTag sample\n- replace global ts-nocheck with targeted typing in zod tests\n- narrow lint suppression scope and add regression assertions --------- Co-authored-by: The Ult <ult_dev@pm.me> Co-authored-by: Melloware <mellowaredev@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 7934b87 commit 6a67a50

File tree

6 files changed

+1071
-300
lines changed

6 files changed

+1071
-300
lines changed
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import { tmpdir } from 'node:os';
2+
import path from 'node:path';
3+
4+
import fs from 'fs-extra';
5+
import { describe, expect, it } from 'vitest';
6+
7+
import { writeZodSchemas, writeZodSchemasFromVerbs } from './write-zod-specs';
8+
9+
type MinimalVerbsContext = {
10+
output: {
11+
override: {
12+
useDates?: boolean;
13+
zod: {
14+
dateTimeOptions?: Record<string, unknown>;
15+
timeOptions?: Record<string, unknown>;
16+
};
17+
};
18+
};
19+
spec: unknown;
20+
target: string;
21+
workspace: string;
22+
};
23+
const createOutputOptions = (): Parameters<typeof writeZodSchemas>[4] =>
24+
({
25+
namingConvention: 'PascalCase',
26+
indexFiles: true,
27+
override: {
28+
zod: {
29+
strict: {
30+
body: true,
31+
},
32+
coerce: {
33+
body: false,
34+
},
35+
},
36+
},
37+
}) as Parameters<typeof writeZodSchemas>[4];
38+
39+
describe('write-zod-specs regressions', () => {
40+
it('writes const constraints before schema export', async () => {
41+
const root = await fs.mkdtemp(path.join(tmpdir(), 'orval-zod-'));
42+
const schemasPath = path.join(root, 'schemas');
43+
44+
const builder = {
45+
spec: {},
46+
target: '',
47+
schemas: [
48+
{
49+
name: 'RangeSchema',
50+
schema: {
51+
type: 'number',
52+
minimum: 2,
53+
maximum: 10,
54+
},
55+
},
56+
],
57+
} satisfies Parameters<typeof writeZodSchemas>[0];
58+
59+
await writeZodSchemas(
60+
builder,
61+
schemasPath,
62+
'.ts',
63+
'',
64+
createOutputOptions(),
65+
);
66+
67+
const filePath = path.join(schemasPath, 'RangeSchema.ts');
68+
const fileContent = await fs.readFile(filePath, 'utf8');
69+
70+
expect(fileContent).toContain('export const RangeSchemaMin = 2;');
71+
expect(fileContent).toContain('export const RangeSchemaMax = 10;');
72+
expect(
73+
fileContent.indexOf('export const RangeSchemaMin = 2;'),
74+
).toBeLessThan(fileContent.indexOf('export const RangeSchema ='));
75+
expect(fileContent).toContain(
76+
'export const RangeSchema = zod.number().min(RangeSchemaMin).max(RangeSchemaMax)',
77+
);
78+
expect(fileContent).not.toContain(
79+
'export const RangeSchema = export const',
80+
);
81+
82+
await fs.remove(root);
83+
});
84+
85+
it('merges case-colliding schema files and keeps canonical index export', async () => {
86+
const root = await fs.mkdtemp(path.join(tmpdir(), 'orval-zod-'));
87+
const schemasPath = path.join(root, 'schemas');
88+
89+
const context = {
90+
output: {
91+
override: {
92+
useDates: false,
93+
zod: {
94+
dateTimeOptions: {},
95+
timeOptions: {},
96+
},
97+
},
98+
},
99+
spec: {},
100+
target: '',
101+
workspace: root,
102+
} satisfies MinimalVerbsContext;
103+
104+
const verbOptions = {
105+
firstVerb: {
106+
operationName: 'fooBar',
107+
originalOperation: {
108+
requestBody: {
109+
content: {
110+
'application/json': {
111+
schema: {
112+
type: 'number',
113+
minimum: 2,
114+
},
115+
},
116+
},
117+
},
118+
parameters: [],
119+
},
120+
response: {
121+
types: {
122+
success: [],
123+
errors: [],
124+
},
125+
},
126+
},
127+
secondVerb: {
128+
operationName: 'Foobar',
129+
originalOperation: {
130+
requestBody: {
131+
content: {
132+
'application/json': {
133+
schema: {
134+
type: 'number',
135+
minimum: 3,
136+
},
137+
},
138+
},
139+
},
140+
parameters: [],
141+
},
142+
response: {
143+
types: {
144+
success: [],
145+
errors: [],
146+
},
147+
},
148+
},
149+
} satisfies Parameters<typeof writeZodSchemasFromVerbs>[0];
150+
151+
await writeZodSchemasFromVerbs(
152+
verbOptions,
153+
schemasPath,
154+
'.ts',
155+
'',
156+
createOutputOptions(),
157+
context,
158+
);
159+
160+
const directoryFiles = await fs.readdir(schemasPath);
161+
const schemaFiles = directoryFiles.filter((file) =>
162+
file.toLowerCase().startsWith('foobarbody.'),
163+
);
164+
165+
expect(schemaFiles).toHaveLength(1);
166+
167+
const mergedFilePath = path.join(schemasPath, schemaFiles[0]);
168+
const mergedContent = await fs.readFile(mergedFilePath, 'utf8');
169+
170+
expect(mergedContent).toContain('export const FooBarBodyMin = 2;');
171+
expect(mergedContent).toContain('export const FoobarBodyMin = 3;');
172+
expect(mergedContent).toContain(
173+
'export const FooBarBody = zod.number().min(FooBarBodyMin)',
174+
);
175+
expect(mergedContent).toContain(
176+
'export const FoobarBody = zod.number().min(FoobarBodyMin)',
177+
);
178+
179+
const indexPath = path.join(schemasPath, 'index.ts');
180+
const indexContent = await fs.readFile(indexPath, 'utf8');
181+
const mergedSchemaExport = path.basename(schemaFiles[0], '.ts');
182+
183+
expect(indexContent).toContain(`export * from './${mergedSchemaExport}';`);
184+
185+
await fs.remove(root);
186+
});
187+
188+
it('writes default const before schema export in split output (#2801)', async () => {
189+
const root = await fs.mkdtemp(path.join(tmpdir(), 'orval-zod-'));
190+
const schemasPath = path.join(root, 'schemas');
191+
192+
const builder = {
193+
spec: {},
194+
target: '',
195+
schemas: [
196+
{
197+
name: 'DefaultedSchema',
198+
schema: {
199+
type: 'string',
200+
default: 'hello',
201+
},
202+
},
203+
],
204+
} satisfies Parameters<typeof writeZodSchemas>[0];
205+
206+
await writeZodSchemas(
207+
builder,
208+
schemasPath,
209+
'.ts',
210+
'',
211+
createOutputOptions(),
212+
);
213+
214+
const filePath = path.join(schemasPath, 'DefaultedSchema.ts');
215+
const fileContent = await fs.readFile(filePath, 'utf8');
216+
217+
expect(fileContent).toContain(
218+
'export const DefaultedSchemaDefault = `hello`;',
219+
);
220+
expect(fileContent).toContain(
221+
'export const DefaultedSchema = zod.string()',
222+
);
223+
expect(
224+
fileContent.indexOf('export const DefaultedSchemaDefault = `hello`;'),
225+
).toBeLessThan(fileContent.indexOf('export const DefaultedSchema ='));
226+
expect(fileContent).not.toContain(
227+
'export const DefaultedSchema = export const',
228+
);
229+
230+
await fs.remove(root);
231+
});
232+
});

0 commit comments

Comments
 (0)