Skip to content

Commit

Permalink
fix(zod): treat additionalProperties keyword (#1443)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
ixth authored Jun 11, 2024
1 parent f350fad commit 257a21f
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 31 deletions.
58 changes: 29 additions & 29 deletions packages/zod/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,20 @@ const resolveZodType = (schemaTypeValue: SchemaObject['type']) => {
let constsUniqueCounter: Record<string, number> = {};

// 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,
context: ContextSpecs,
_required: boolean | undefined,
name: string,
strict: boolean,
): { functions: [string, any][]; consts: string[] } => {
): ZodValidationSchemaDefinition => {
if (!schema) return { functions: [], consts: [] };

const consts: string[] = [];
Expand Down Expand Up @@ -223,15 +228,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;
Expand Down Expand Up @@ -291,14 +296,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 } => {
Expand Down Expand Up @@ -359,10 +359,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')}
Expand All @@ -386,11 +386,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})`;
}
Expand Down Expand Up @@ -460,7 +460,7 @@ const parseBodyAndResponse = ({
name: string;
strict: boolean;
}): {
input: ZodValidationSchemaDefinitionInput;
input: ZodValidationSchemaDefinition;
isArray: boolean;
} => {
if (!data) {
Expand Down Expand Up @@ -530,9 +530,9 @@ const parseParameters = ({
response: boolean;
};
}): {
headers: ZodValidationSchemaDefinitionInput;
queryParams: ZodValidationSchemaDefinitionInput;
params: ZodValidationSchemaDefinitionInput;
headers: ZodValidationSchemaDefinition;
queryParams: ZodValidationSchemaDefinition;
params: ZodValidationSchemaDefinition;
} => {
if (!data) {
return {
Expand Down Expand Up @@ -608,7 +608,7 @@ const parseParameters = ({
>,
);

const headers: ZodValidationSchemaDefinitionInput = {
const headers: ZodValidationSchemaDefinition = {
functions: [],
consts: [],
};
Expand All @@ -621,7 +621,7 @@ const parseParameters = ({
}
}

const queryParams: ZodValidationSchemaDefinitionInput = {
const queryParams: ZodValidationSchemaDefinition = {
functions: [],
consts: [],
};
Expand All @@ -634,7 +634,7 @@ const parseParameters = ({
}
}

const params: ZodValidationSchemaDefinitionInput = {
const params: ZodValidationSchemaDefinition = {
functions: [],
consts: [],
};
Expand Down
113 changes: 111 additions & 2 deletions packages/zod/src/zod.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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: [],
});
});
});
9 changes: 9 additions & 0 deletions tests/configs/zod.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,13 @@ export default defineConfig({
target: '../specifications/circular.yaml',
},
},
additionalProperties: {
output: {
target: '../generated/zod',
client: 'zod',
},
input: {
target: '../specifications/additional-properties.yaml',
},
},
});

0 comments on commit 257a21f

Please sign in to comment.