diff --git a/packages/zod/src/index.ts b/packages/zod/src/index.ts index 5d26950af..15b8fd4c0 100644 --- a/packages/zod/src/index.ts +++ b/packages/zod/src/index.ts @@ -70,7 +70,12 @@ const resolveZodType = (schemaTypeValue: SchemaObject['type']) => { let constsUniqueCounter: Record = {}; // https://github.com/colinhacks/zod#coercion-for-primitives -const COERCEABLE_TYPES = ['string', 'number', 'boolean', 'bigint', 'date']; +const COERCIBLE_TYPES = ['string', 'number', 'boolean', 'bigint', 'date']; + +export type ZodValidationSchemaDefinition = { + functions: [string, any][]; + consts: string[]; +}; export const generateZodValidationSchemaDefinition = ( schema: SchemaObject | undefined, @@ -78,7 +83,7 @@ export const generateZodValidationSchemaDefinition = ( _required: boolean | undefined, name: string, strict: boolean, -): { functions: [string, any][]; consts: string[] } => { +): ZodValidationSchemaDefinition => { if (!schema) return { functions: [], consts: [] }; const consts: string[] = []; @@ -224,15 +229,15 @@ export const generateZodValidationSchemaDefinition = ( if (schema.additionalProperties) { functions.push([ 'additionalProperties', - isBoolean(schema.additionalProperties) - ? schema.additionalProperties - : generateZodValidationSchemaDefinition( - schema.additionalProperties as SchemaObject, - context, - true, - name, - strict, - ), + generateZodValidationSchemaDefinition( + isBoolean(schema.additionalProperties) + ? {} + : (schema.additionalProperties as SchemaObject), + context, + true, + name, + strict, + ), ]); break; @@ -292,14 +297,9 @@ export const generateZodValidationSchemaDefinition = ( return { functions, consts: uniq(consts) }; }; -export type ZodValidationSchemaDefinitionInput = { - functions: [string, any][]; - consts: string[]; -}; - export const parseZodValidationSchemaDefinition = ( - input: ZodValidationSchemaDefinitionInput, - contex: ContextSpecs, + input: ZodValidationSchemaDefinition, + context: ContextSpecs, coerceTypes: boolean | ZodCoerceType[] = false, preprocessResponse?: GeneratorMutator, ): { zod: string; consts: string } => { @@ -360,10 +360,10 @@ export const parseZodValidationSchemaDefinition = ( return `zod.object({ ${Object.entries(args) .map(([key, schema]) => { - const value = (schema as ZodValidationSchemaDefinitionInput).functions + const value = (schema as ZodValidationSchemaDefinition).functions .map(parseProperty) .join(''); - consts += (schema as ZodValidationSchemaDefinitionInput).consts.join('\n'); + consts += (schema as ZodValidationSchemaDefinition).consts.join('\n'); return ` "${key}": ${value.startsWith('.') ? 'zod' : ''}${value}`; }) .join(',\n')} @@ -387,11 +387,11 @@ ${Object.entries(args) coerceTypes && (Array.isArray(coerceTypes) ? coerceTypes.includes(fn as ZodCoerceType) - : COERCEABLE_TYPES.includes(fn)); + : COERCIBLE_TYPES.includes(fn)); if ( (fn !== 'date' && shouldCoerceType) || - (fn === 'date' && shouldCoerceType && contex.output.override.useDates) + (fn === 'date' && shouldCoerceType && context.output.override.useDates) ) { return `.coerce.${fn}(${args})`; } @@ -461,7 +461,7 @@ const parseBodyAndResponse = ({ name: string; strict: boolean; }): { - input: ZodValidationSchemaDefinitionInput; + input: ZodValidationSchemaDefinition; isArray: boolean; } => { if (!data) { @@ -531,9 +531,9 @@ const parseParameters = ({ response: boolean; }; }): { - headers: ZodValidationSchemaDefinitionInput; - queryParams: ZodValidationSchemaDefinitionInput; - params: ZodValidationSchemaDefinitionInput; + headers: ZodValidationSchemaDefinition; + queryParams: ZodValidationSchemaDefinition; + params: ZodValidationSchemaDefinition; } => { if (!data) { return { @@ -609,7 +609,7 @@ const parseParameters = ({ >, ); - const headers: ZodValidationSchemaDefinitionInput = { + const headers: ZodValidationSchemaDefinition = { functions: [], consts: [], }; @@ -622,7 +622,7 @@ const parseParameters = ({ } } - const queryParams: ZodValidationSchemaDefinitionInput = { + const queryParams: ZodValidationSchemaDefinition = { functions: [], consts: [], }; @@ -635,7 +635,7 @@ const parseParameters = ({ } } - const params: ZodValidationSchemaDefinitionInput = { + const params: ZodValidationSchemaDefinition = { functions: [], consts: [], }; diff --git a/packages/zod/src/zod.test.ts b/packages/zod/src/zod.test.ts index 07b327110..13135eddf 100644 --- a/packages/zod/src/zod.test.ts +++ b/packages/zod/src/zod.test.ts @@ -1,13 +1,13 @@ import { describe, expect, it } from 'vitest'; import { - type ZodValidationSchemaDefinitionInput, + type ZodValidationSchemaDefinition, parseZodValidationSchemaDefinition, generateZodValidationSchemaDefinition, } from '.'; import { SchemaObject } from 'openapi3-ts/oas30'; import { ContextSpecs } from '@orval/core'; -const queryParams: ZodValidationSchemaDefinitionInput = { +const queryParams: ZodValidationSchemaDefinition = { functions: [ [ 'object', @@ -42,6 +42,29 @@ const queryParams: ZodValidationSchemaDefinitionInput = { consts: [], }; +const record: ZodValidationSchemaDefinition = { + functions: [ + [ + 'object', + { + queryParams: { + functions: [ + [ + 'additionalProperties', + { + functions: [['any', undefined]], + consts: [], + }, + ], + ], + consts: [], + }, + }, + ], + ], + consts: [], +}; + describe('parseZodValidationSchemaDefinition', () => { describe('with `override.coerceTypes = false` (default)', () => { it('does not emit coerced zod property schemas', () => { @@ -82,6 +105,24 @@ describe('parseZodValidationSchemaDefinition', () => { ); }); }); + + it('treats additionalProperties properly', () => { + const parseResult = parseZodValidationSchemaDefinition( + record, + { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs, + false, + ); + + expect(parseResult.zod).toBe( + 'zod.object({\n "queryParams": zod.record(zod.string(), zod.any())\n})', + ); + }); }); const objectIntoObjectSchema: SchemaObject = { @@ -119,6 +160,20 @@ const deepRequiredSchema: SchemaObject = { }, }; +const additionalPropertiesSchema: SchemaObject = { + type: 'object', + properties: { + any: { + type: 'object', + additionalProperties: {}, + }, + true: { + type: 'object', + additionalProperties: true, + }, + }, +}; + describe('generateZodValidationSchemaDefinition`', () => { it('required', () => { const result = generateZodValidationSchemaDefinition( @@ -224,4 +279,58 @@ describe('generateZodValidationSchemaDefinition`', () => { consts: [], }); }); + + it('additionalProperties', () => { + const result = generateZodValidationSchemaDefinition( + additionalPropertiesSchema, + { + output: { + override: { + useDates: false, + }, + }, + } as ContextSpecs, + true, + 'strict', + true, + ); + + expect(result).toEqual({ + functions: [ + [ + 'object', + { + any: { + functions: [ + [ + 'additionalProperties', + { + functions: [['any', undefined]], + consts: [], + }, + ], + ['optional', undefined], + ], + consts: [], + }, + true: { + functions: [ + [ + 'additionalProperties', + { + functions: [['any', undefined]], + consts: [], + }, + ], + ['optional', undefined], + ], + consts: [], + }, + }, + ], + ['strict', undefined], + ], + consts: [], + }); + }); }); diff --git a/tests/configs/zod.config.ts b/tests/configs/zod.config.ts index ffce668af..2a4ea3a77 100644 --- a/tests/configs/zod.config.ts +++ b/tests/configs/zod.config.ts @@ -128,4 +128,13 @@ export default defineConfig({ target: '../specifications/circular.yaml', }, }, + additionalProperties: { + output: { + target: '../generated/zod', + client: 'zod', + }, + input: { + target: '../specifications/additional-properties.yaml', + }, + }, });