From 468b1fa686f257284287a73b53dd280327c5bca8 Mon Sep 17 00:00:00 2001 From: Sam Chung Date: Fri, 1 Mar 2024 16:54:03 +1100 Subject: [PATCH] Add support for ZodLazy, ZodPipeline, ZodBrand and ZodEffects for parameter schemas (#229) Co-authored-by: Andy Edwards --- src/create/document.ts | 11 +++++- src/create/parameters.test.ts | 70 +++++++++++++++++++++++++++++++++++ src/create/parameters.ts | 41 ++++++++++++++++---- 3 files changed, 113 insertions(+), 9 deletions(-) diff --git a/src/create/document.ts b/src/create/document.ts index 876c5f0..b0c9c93 100644 --- a/src/create/document.ts +++ b/src/create/document.ts @@ -1,4 +1,4 @@ -import type { AnyZodObject, ZodType } from 'zod'; +import type { AnyZodObject, ZodType, ZodTypeDef } from 'zod'; import type { OpenApiVersion } from '../openapi'; import type { oas30, oas31 } from '../openapi3-ts/dist'; @@ -46,7 +46,8 @@ export interface ZodOpenApiResponsesObject } export type ZodOpenApiParameters = { - [type in oas31.ParameterLocation & oas30.ParameterLocation]?: AnyZodObject; + [type in oas31.ParameterLocation & + oas30.ParameterLocation]?: ZodObjectInputType; }; export interface ZodOpenApiOperationObject @@ -132,6 +133,12 @@ export interface ZodOpenApiObject components?: ZodOpenApiComponentsObject; } +export type ZodObjectInputType< + Output = unknown, + Def extends ZodTypeDef = ZodTypeDef, + Input = Record, +> = ZodType; + export const createDocument = ( zodOpenApiObject: ZodOpenApiObject, ): oas31.OpenAPIObject => { diff --git a/src/create/parameters.test.ts b/src/create/parameters.test.ts index ae9e5a1..93760f6 100644 --- a/src/create/parameters.test.ts +++ b/src/create/parameters.test.ts @@ -267,4 +267,74 @@ describe('createParametersObject', () => { expect(result).toStrictEqual(expectedResult); }); + + it('should support wrapped ZodObject schema', () => { + const expectedResult: oas31.OperationObject['parameters'] = [ + { + in: 'path', + name: 'b', + schema: { + type: 'string', + }, + required: true, + }, + { + in: 'query', + name: 'a', + schema: { + type: 'string', + }, + required: true, + }, + { + in: 'cookie', + name: 'c', + schema: { + type: 'string', + }, + required: true, + }, + { + in: 'header', + name: 'd', + schema: { + type: 'string', + }, + required: true, + }, + ]; + const result = createParametersObject( + [], + { + path: z + .lazy(() => + z.object({ b: z.string() }).pipe(z.object({ b: z.string() })), + ) + .brand('test') + .transform((x) => x), + query: z + .lazy(() => + z.object({ a: z.string() }).pipe(z.object({ a: z.string() })), + ) + .brand('test') + .transform((x) => x), + cookie: z + .lazy(() => + z.object({ c: z.string() }).pipe(z.object({ c: z.string() })), + ) + .brand('test') + .transform((x) => x), + header: z + .lazy(() => + z.object({ d: z.string() }).pipe(z.object({ d: z.string() })), + ) + .brand('test') + .transform((x) => x), + }, + getDefaultComponents(), + ['parameters'], + ); + + expect(result).toStrictEqual(expectedResult); + }); }); diff --git a/src/create/parameters.ts b/src/create/parameters.ts index fb18993..c0cf623 100644 --- a/src/create/parameters.ts +++ b/src/create/parameters.ts @@ -1,10 +1,10 @@ import type { AnyZodObject, ZodRawShape, ZodType } from 'zod'; import type { oas30, oas31 } from '../openapi3-ts/dist'; -import { isAnyZodType } from '../zodType'; +import { isAnyZodType, isZodType } from '../zodType'; import type { ComponentsObject } from './components'; -import type { ZodOpenApiParameters } from './document'; +import type { ZodObjectInputType, ZodOpenApiParameters } from './document'; import { type SchemaState, createSchema } from './schema'; import { isOptionalSchema } from './schema/parsers/optional'; @@ -101,17 +101,18 @@ export const createParamOrRef = ( const createParameters = ( type: keyof ZodOpenApiParameters, - zodObject: AnyZodObject | undefined, + zodObjectType: ZodObjectInputType | undefined, components: ComponentsObject, subpath: string[], ): Array => { - if (!zodObject) { + if (!zodObjectType) { return []; } - return Object.entries(zodObject.shape as ZodRawShape).map( - ([key, zodSchema]: [string, ZodType]) => - createParamOrRef(zodSchema, components, [...subpath, key], type, key), + const zodObject = getZodObject(zodObjectType, 'input').shape as ZodRawShape; + + return Object.entries(zodObject).map(([key, zodSchema]: [string, ZodType]) => + createParamOrRef(zodSchema, components, [...subpath, key], type, key), ); }; @@ -200,3 +201,29 @@ export const createParametersObject = ( return combinedParameters.length ? combinedParameters : undefined; }; + +export const getZodObject = ( + schema: ZodObjectInputType, + type: 'input' | 'output', +): AnyZodObject => { + if (isZodType(schema, 'ZodObject')) { + return schema; + } + if (isZodType(schema, 'ZodLazy')) { + return getZodObject(schema.schema as ZodObjectInputType, type); + } + if (isZodType(schema, 'ZodEffects')) { + return getZodObject(schema.innerType() as ZodObjectInputType, type); + } + if (isZodType(schema, 'ZodBranded')) { + return getZodObject(schema.unwrap() as ZodObjectInputType, type); + } + if (isZodType(schema, 'ZodPipeline')) { + if (type === 'input') { + return getZodObject(schema._def.in as ZodObjectInputType, type); + } + return getZodObject(schema._def.out as ZodObjectInputType, type); + } + + throw new Error('failed to find ZodObject in schema'); +};