Skip to content

Commit 257a21f

Browse files
authoredJun 11, 2024
fix(zod): treat additionalProperties keyword (#1443)
* chore(zod): typos * refactor(zod): pull up definition type * test(zod): add tests for additionalProperties keyword * fix(zod): fix handling of additionalProperties in zod generator
1 parent f350fad commit 257a21f

File tree

3 files changed

+149
-31
lines changed

3 files changed

+149
-31
lines changed
 

‎packages/zod/src/index.ts

+29-29
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,20 @@ const resolveZodType = (schemaTypeValue: SchemaObject['type']) => {
6969
let constsUniqueCounter: Record<string, number> = {};
7070

7171
// https://github.com/colinhacks/zod#coercion-for-primitives
72-
const COERCEABLE_TYPES = ['string', 'number', 'boolean', 'bigint', 'date'];
72+
const COERCIBLE_TYPES = ['string', 'number', 'boolean', 'bigint', 'date'];
73+
74+
export type ZodValidationSchemaDefinition = {
75+
functions: [string, any][];
76+
consts: string[];
77+
};
7378

7479
export const generateZodValidationSchemaDefinition = (
7580
schema: SchemaObject | undefined,
7681
context: ContextSpecs,
7782
_required: boolean | undefined,
7883
name: string,
7984
strict: boolean,
80-
): { functions: [string, any][]; consts: string[] } => {
85+
): ZodValidationSchemaDefinition => {
8186
if (!schema) return { functions: [], consts: [] };
8287

8388
const consts: string[] = [];
@@ -223,15 +228,15 @@ export const generateZodValidationSchemaDefinition = (
223228
if (schema.additionalProperties) {
224229
functions.push([
225230
'additionalProperties',
226-
isBoolean(schema.additionalProperties)
227-
? schema.additionalProperties
228-
: generateZodValidationSchemaDefinition(
229-
schema.additionalProperties as SchemaObject,
230-
context,
231-
true,
232-
name,
233-
strict,
234-
),
231+
generateZodValidationSchemaDefinition(
232+
isBoolean(schema.additionalProperties)
233+
? {}
234+
: (schema.additionalProperties as SchemaObject),
235+
context,
236+
true,
237+
name,
238+
strict,
239+
),
235240
]);
236241

237242
break;
@@ -291,14 +296,9 @@ export const generateZodValidationSchemaDefinition = (
291296
return { functions, consts: uniq(consts) };
292297
};
293298

294-
export type ZodValidationSchemaDefinitionInput = {
295-
functions: [string, any][];
296-
consts: string[];
297-
};
298-
299299
export const parseZodValidationSchemaDefinition = (
300-
input: ZodValidationSchemaDefinitionInput,
301-
contex: ContextSpecs,
300+
input: ZodValidationSchemaDefinition,
301+
context: ContextSpecs,
302302
coerceTypes: boolean | ZodCoerceType[] = false,
303303
preprocessResponse?: GeneratorMutator,
304304
): { zod: string; consts: string } => {
@@ -359,10 +359,10 @@ export const parseZodValidationSchemaDefinition = (
359359
return `zod.object({
360360
${Object.entries(args)
361361
.map(([key, schema]) => {
362-
const value = (schema as ZodValidationSchemaDefinitionInput).functions
362+
const value = (schema as ZodValidationSchemaDefinition).functions
363363
.map(parseProperty)
364364
.join('');
365-
consts += (schema as ZodValidationSchemaDefinitionInput).consts.join('\n');
365+
consts += (schema as ZodValidationSchemaDefinition).consts.join('\n');
366366
return ` "${key}": ${value.startsWith('.') ? 'zod' : ''}${value}`;
367367
})
368368
.join(',\n')}
@@ -386,11 +386,11 @@ ${Object.entries(args)
386386
coerceTypes &&
387387
(Array.isArray(coerceTypes)
388388
? coerceTypes.includes(fn as ZodCoerceType)
389-
: COERCEABLE_TYPES.includes(fn));
389+
: COERCIBLE_TYPES.includes(fn));
390390

391391
if (
392392
(fn !== 'date' && shouldCoerceType) ||
393-
(fn === 'date' && shouldCoerceType && contex.output.override.useDates)
393+
(fn === 'date' && shouldCoerceType && context.output.override.useDates)
394394
) {
395395
return `.coerce.${fn}(${args})`;
396396
}
@@ -460,7 +460,7 @@ const parseBodyAndResponse = ({
460460
name: string;
461461
strict: boolean;
462462
}): {
463-
input: ZodValidationSchemaDefinitionInput;
463+
input: ZodValidationSchemaDefinition;
464464
isArray: boolean;
465465
} => {
466466
if (!data) {
@@ -530,9 +530,9 @@ const parseParameters = ({
530530
response: boolean;
531531
};
532532
}): {
533-
headers: ZodValidationSchemaDefinitionInput;
534-
queryParams: ZodValidationSchemaDefinitionInput;
535-
params: ZodValidationSchemaDefinitionInput;
533+
headers: ZodValidationSchemaDefinition;
534+
queryParams: ZodValidationSchemaDefinition;
535+
params: ZodValidationSchemaDefinition;
536536
} => {
537537
if (!data) {
538538
return {
@@ -608,7 +608,7 @@ const parseParameters = ({
608608
>,
609609
);
610610

611-
const headers: ZodValidationSchemaDefinitionInput = {
611+
const headers: ZodValidationSchemaDefinition = {
612612
functions: [],
613613
consts: [],
614614
};
@@ -621,7 +621,7 @@ const parseParameters = ({
621621
}
622622
}
623623

624-
const queryParams: ZodValidationSchemaDefinitionInput = {
624+
const queryParams: ZodValidationSchemaDefinition = {
625625
functions: [],
626626
consts: [],
627627
};
@@ -634,7 +634,7 @@ const parseParameters = ({
634634
}
635635
}
636636

637-
const params: ZodValidationSchemaDefinitionInput = {
637+
const params: ZodValidationSchemaDefinition = {
638638
functions: [],
639639
consts: [],
640640
};

‎packages/zod/src/zod.test.ts

+111-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { describe, expect, it } from 'vitest';
22
import {
3-
type ZodValidationSchemaDefinitionInput,
3+
type ZodValidationSchemaDefinition,
44
parseZodValidationSchemaDefinition,
55
generateZodValidationSchemaDefinition,
66
} from '.';
77
import { SchemaObject } from 'openapi3-ts/oas30';
88
import { ContextSpecs } from '@orval/core';
99

10-
const queryParams: ZodValidationSchemaDefinitionInput = {
10+
const queryParams: ZodValidationSchemaDefinition = {
1111
functions: [
1212
[
1313
'object',
@@ -42,6 +42,29 @@ const queryParams: ZodValidationSchemaDefinitionInput = {
4242
consts: [],
4343
};
4444

45+
const record: ZodValidationSchemaDefinition = {
46+
functions: [
47+
[
48+
'object',
49+
{
50+
queryParams: {
51+
functions: [
52+
[
53+
'additionalProperties',
54+
{
55+
functions: [['any', undefined]],
56+
consts: [],
57+
},
58+
],
59+
],
60+
consts: [],
61+
},
62+
},
63+
],
64+
],
65+
consts: [],
66+
};
67+
4568
describe('parseZodValidationSchemaDefinition', () => {
4669
describe('with `override.coerceTypes = false` (default)', () => {
4770
it('does not emit coerced zod property schemas', () => {
@@ -82,6 +105,24 @@ describe('parseZodValidationSchemaDefinition', () => {
82105
);
83106
});
84107
});
108+
109+
it('treats additionalProperties properly', () => {
110+
const parseResult = parseZodValidationSchemaDefinition(
111+
record,
112+
{
113+
output: {
114+
override: {
115+
useDates: false,
116+
},
117+
},
118+
} as ContextSpecs,
119+
false,
120+
);
121+
122+
expect(parseResult.zod).toBe(
123+
'zod.object({\n "queryParams": zod.record(zod.string(), zod.any())\n})',
124+
);
125+
});
85126
});
86127

87128
const objectIntoObjectSchema: SchemaObject = {
@@ -119,6 +160,20 @@ const deepRequiredSchema: SchemaObject = {
119160
},
120161
};
121162

163+
const additionalPropertiesSchema: SchemaObject = {
164+
type: 'object',
165+
properties: {
166+
any: {
167+
type: 'object',
168+
additionalProperties: {},
169+
},
170+
true: {
171+
type: 'object',
172+
additionalProperties: true,
173+
},
174+
},
175+
};
176+
122177
describe('generateZodValidationSchemaDefinition`', () => {
123178
it('required', () => {
124179
const result = generateZodValidationSchemaDefinition(
@@ -224,4 +279,58 @@ describe('generateZodValidationSchemaDefinition`', () => {
224279
consts: [],
225280
});
226281
});
282+
283+
it('additionalProperties', () => {
284+
const result = generateZodValidationSchemaDefinition(
285+
additionalPropertiesSchema,
286+
{
287+
output: {
288+
override: {
289+
useDates: false,
290+
},
291+
},
292+
} as ContextSpecs,
293+
true,
294+
'strict',
295+
true,
296+
);
297+
298+
expect(result).toEqual({
299+
functions: [
300+
[
301+
'object',
302+
{
303+
any: {
304+
functions: [
305+
[
306+
'additionalProperties',
307+
{
308+
functions: [['any', undefined]],
309+
consts: [],
310+
},
311+
],
312+
['optional', undefined],
313+
],
314+
consts: [],
315+
},
316+
true: {
317+
functions: [
318+
[
319+
'additionalProperties',
320+
{
321+
functions: [['any', undefined]],
322+
consts: [],
323+
},
324+
],
325+
['optional', undefined],
326+
],
327+
consts: [],
328+
},
329+
},
330+
],
331+
['strict', undefined],
332+
],
333+
consts: [],
334+
});
335+
});
227336
});

‎tests/configs/zod.config.ts

+9
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,13 @@ export default defineConfig({
128128
target: '../specifications/circular.yaml',
129129
},
130130
},
131+
additionalProperties: {
132+
output: {
133+
target: '../generated/zod',
134+
client: 'zod',
135+
},
136+
input: {
137+
target: '../specifications/additional-properties.yaml',
138+
},
139+
},
131140
});

0 commit comments

Comments
 (0)
Please sign in to comment.