diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index bbea1ba..331c668 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -1,8 +1,10 @@ name: Validate on: - - pull_request - - push + pull_request: + push: + branches: + - 'master' permissions: {} jobs: diff --git a/README.md b/README.md index 22a87ab..1d961bb 100644 --- a/README.md +++ b/README.md @@ -298,7 +298,7 @@ Wherever `title` is used in schemas across the document, it will instead be crea This can be an extremely powerful way to generate better Open API documentation. There are some Open API features like [discriminator mapping](https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/) which require all schemas in the union to contain a ref. -To display components which are not referenced by simply add the Zod Schema to the schema components directly. +To display components which are not referenced in the responses or requests simply add the Zod Schema to the schema components directly. eg. @@ -306,12 +306,18 @@ eg. { "components": { "schemas": { - MyJobSchema // note: this will register this Zod Schema as MyJobSchema unless `ref` is specified on the type + MyJobSchema // note: this will register this Zod Schema as MyJobSchema unless `ref` in `openapi()` is specified on the type } } } ``` +##### Zod Effects + +`.transform()` and `.preprocess()` are complicated because they are technically two types (input & output). This means that we need to understand which type you are after. This means if you are adding the ZodSchema directly to the `components` section, we need to know whether you want the response or request type created. You can do this by setting the `refType` field to `input` or `output` in `.openapi()`. This defaults to `output` by default. + +If you use a registered schema with a ZodEffect in both a request and response schema you will receive an error because we cannot register two different schemas under the same `ref`. + #### Parameters Query, Path, Header & Cookie parameters can be similarly registered: @@ -356,7 +362,9 @@ const header = z.string().openapi({ - ZodDiscriminatedUnion - `discriminator` mapping when all schemas in the union contain a `ref`. - ZodEffects - - `pre-process` and `refine` support + - `transform` support for request schemas. Wrap your transform in a ZodPipeline to enable response schema creation or declare a manual `type` in the `.openapi()` section of that schema. + - `pre-process` support for response schemas. Wrap your transform in a ZodPipeline to enable request schema creation or declare a manual `type` in the `.openapi()` section of that schema. + - `refine` full support. - ZodEnum - ZodLiteral - ZodNativeEnum @@ -368,6 +376,7 @@ const header = z.string().openapi({ - `exclusiveMin`/`min`/`exclusiveMax`/`max` mapping for `.min()`, `.max()`, `lt()`, `gt()` - ZodObject - ZodOptional +- ZodPipeline - ZodRecord - ZodString - `format` mapping for `.url()`, `.uuid()`, `.email()`, `.datetime()` diff --git a/src/create/components.test.ts b/src/create/components.test.ts index 491b42c..c5750de 100644 --- a/src/create/components.test.ts +++ b/src/create/components.test.ts @@ -67,6 +67,7 @@ describe('getDefaultComponents', () => { type: 'string', }, zodSchema: aSchema, + types: ['input', 'output'], }, b: { schemaObject: { @@ -149,6 +150,7 @@ describe('createComponents', () => { type: 'string', }, zodSchema: z.string().openapi({ ref: 'a' }), + types: ['output'], }, }, headers: { @@ -224,6 +226,7 @@ describe('createComponents', () => { type: 'string', }, zodSchema: z.string().openapi({ ref: 'a' }), + types: ['output'], }, }, headers: { diff --git a/src/create/components.ts b/src/create/components.ts index 5292eb3..cec3de4 100644 --- a/src/create/components.ts +++ b/src/create/components.ts @@ -2,22 +2,26 @@ import { oas30, oas31 } from 'openapi3-ts'; import { ZodType } from 'zod'; import { ZodOpenApiComponentsObject, ZodOpenApiVersion } from './document'; +import { SchemaState } from './schema'; import { createSchemaWithMetadata } from './schema/metadata'; -export interface Schema { +export type CreationType = 'input' | 'output'; + +export interface SchemaComponent { zodSchema?: ZodType; schemaObject: | oas31.SchemaObject | oas31.ReferenceObject | oas30.SchemaObject | oas30.ReferenceObject; + types?: [CreationType, ...CreationType[]]; } interface SchemaComponentObject { - [ref: string]: Schema | undefined; + [ref: string]: SchemaComponent | undefined; } -export interface Parameter { +export interface ParameterComponent { zodSchema?: ZodType; paramObject: | oas31.ParameterObject @@ -27,7 +31,7 @@ export interface Parameter { } interface ParametersComponentObject { - [ref: string]: Parameter | undefined; + [ref: string]: ParameterComponent | undefined; } export interface Header { @@ -90,9 +94,14 @@ const createSchemas = ( } if (schema instanceof ZodType) { + const state: SchemaState = { + components, + type: schema._def.openapi?.refType ?? 'output', + }; components.schemas[ref] = { - schemaObject: createSchemaWithMetadata(schema, components), + schemaObject: createSchemaWithMetadata(schema, state), zodSchema: schema, + types: state.effectType ? [state.effectType] : ['input', 'output'], }; return; } diff --git a/src/create/content.test.ts b/src/create/content.test.ts index 22bd176..5f9a0c0 100644 --- a/src/create/content.test.ts +++ b/src/create/content.test.ts @@ -31,6 +31,7 @@ describe('createContent', () => { }, }, getDefaultComponents(), + 'output', ); expect(result).toStrictEqual(expectedResult); @@ -66,6 +67,7 @@ describe('createContent', () => { }, }, getDefaultComponents(), + 'output', ); expect(result).toStrictEqual(expectedResult); @@ -99,6 +101,7 @@ describe('createContent', () => { }, }, getDefaultComponents(), + 'output', ); expect(result).toStrictEqual(expectedResult); diff --git a/src/create/content.ts b/src/create/content.ts index 187a4b1..459778a 100644 --- a/src/create/content.ts +++ b/src/create/content.ts @@ -1,7 +1,7 @@ import { oas31 } from 'openapi3-ts'; import { AnyZodObject, ZodType } from 'zod'; -import { ComponentsObject } from './components'; +import { ComponentsObject, CreationType } from './components'; import { ZodOpenApiContentObject, ZodOpenApiMediaTypeObject } from './document'; import { createSchemaOrRef } from './schema'; @@ -12,6 +12,7 @@ const createMediaTypeSchema = ( | oas31.ReferenceObject | undefined, components: ComponentsObject, + type: CreationType, ): oas31.SchemaObject | oas31.ReferenceObject | undefined => { if (!schemaObject) { return undefined; @@ -21,12 +22,16 @@ const createMediaTypeSchema = ( return schemaObject; } - return createSchemaOrRef(schemaObject, components); + return createSchemaOrRef(schemaObject, { + components, + type, + }); }; const createMediaTypeObject = ( mediaTypeObject: ZodOpenApiMediaTypeObject | undefined, components: ComponentsObject, + type: CreationType, ): oas31.MediaTypeObject | undefined => { if (!mediaTypeObject) { return undefined; @@ -34,19 +39,21 @@ const createMediaTypeObject = ( return { ...mediaTypeObject, - schema: createMediaTypeSchema(mediaTypeObject.schema, components), + schema: createMediaTypeSchema(mediaTypeObject.schema, components, type), }; }; export const createContent = ( contentObject: ZodOpenApiContentObject, components: ComponentsObject, + type: CreationType, ): oas31.ContentObject => Object.entries(contentObject).reduce( (acc, [path, zodOpenApiMediaTypeObject]): oas31.ContentObject => { const mediaTypeObject = createMediaTypeObject( zodOpenApiMediaTypeObject, components, + type, ); if (mediaTypeObject) { diff --git a/src/create/parameters.ts b/src/create/parameters.ts index a79a380..7d33c23 100644 --- a/src/create/parameters.ts +++ b/src/create/parameters.ts @@ -13,7 +13,10 @@ export const createBaseParameter = ( components: ComponentsObject, ): oas31.BaseParameterObject => { const { ref, ...rest } = schema._def.openapi?.param ?? {}; - const schemaOrRef = createSchemaOrRef(schema, components); + const schemaOrRef = createSchemaOrRef(schema, { + components, + type: 'input', + }); const required = !schema.isOptional(); return { ...rest, diff --git a/src/create/paths.ts b/src/create/paths.ts index 440b1c7..a3feb49 100644 --- a/src/create/paths.ts +++ b/src/create/paths.ts @@ -21,7 +21,7 @@ const createRequestBody = ( } return { ...requestBodyObject, - content: createContent(requestBodyObject.content, components), + content: createContent(requestBodyObject.content, components, 'input'), }; }; diff --git a/src/create/responses.ts b/src/create/responses.ts index 50b61c3..15fd324 100644 --- a/src/create/responses.ts +++ b/src/create/responses.ts @@ -45,7 +45,10 @@ export const createBaseHeader = ( components: ComponentsObject, ): oas31.BaseParameterObject => { const { ref, ...rest } = schema._def.openapi?.header ?? {}; - const schemaOrRef = createSchemaOrRef(schema, components); + const schemaOrRef = createSchemaOrRef(schema, { + components, + type: 'input', + }); const required = !schema.isOptional(); return { ...rest, @@ -123,7 +126,7 @@ const createResponse = ( return { ...rest, ...(maybeHeaders && { headers: maybeHeaders }), - ...(content && { content: createContent(content, components) }), + ...(content && { content: createContent(content, components, 'output') }), }; }; diff --git a/src/create/schema/array.test.ts b/src/create/schema/array.test.ts index dada204..187a24d 100644 --- a/src/create/schema/array.test.ts +++ b/src/create/schema/array.test.ts @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { createOutputState } from '../../testing/state'; import { createArraySchema } from './array'; @@ -18,7 +18,7 @@ describe('createArraySchema', () => { }; const schema = z.array(z.string()); - const result = createArraySchema(schema, getDefaultComponents()); + const result = createArraySchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -34,7 +34,7 @@ describe('createArraySchema', () => { }; const schema = z.array(z.string()).min(0).max(10); - const result = createArraySchema(schema, getDefaultComponents()); + const result = createArraySchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -50,7 +50,7 @@ describe('createArraySchema', () => { }; const schema = z.array(z.string()).length(10); - const result = createArraySchema(schema, getDefaultComponents()); + const result = createArraySchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/array.ts b/src/create/schema/array.ts index abc72fb..aafe0bc 100644 --- a/src/create/schema/array.ts +++ b/src/create/schema/array.ts @@ -1,13 +1,11 @@ import { oas31 } from 'openapi3-ts'; import { ZodArray, ZodTypeAny } from 'zod'; -import { ComponentsObject } from '../components'; - -import { createSchemaOrRef } from '.'; +import { SchemaState, createSchemaOrRef } from '.'; export const createArraySchema = ( zodArray: ZodArray, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject => { const zodType = zodArray._def.type as ZodTypeAny; const minItems = @@ -16,7 +14,7 @@ export const createArraySchema = ( zodArray._def.exactLength?.value ?? zodArray._def.maxLength?.value; return { type: 'array', - items: createSchemaOrRef(zodType, components), + items: createSchemaOrRef(zodType, state), ...(minItems !== undefined && { minItems }), ...(maxItems !== undefined && { maxItems }), }; diff --git a/src/create/schema/catch.test.ts b/src/create/schema/catch.test.ts index 72f7633..b53ed91 100644 --- a/src/create/schema/catch.test.ts +++ b/src/create/schema/catch.test.ts @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { createOutputState } from '../../testing/state'; import { createCatchSchema } from './catch'; @@ -15,7 +15,7 @@ describe('createCatchSchema', () => { }; const schema = z.string().catch('bob'); - const result = createCatchSchema(schema, getDefaultComponents()); + const result = createCatchSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/catch.ts b/src/create/schema/catch.ts index 2eb5572..81fce95 100644 --- a/src/create/schema/catch.ts +++ b/src/create/schema/catch.ts @@ -1,12 +1,10 @@ import { oas31 } from 'openapi3-ts'; import { ZodCatch, ZodType } from 'zod'; -import { ComponentsObject } from '../components'; - -import { createSchemaOrRef } from '.'; +import { SchemaState, createSchemaOrRef } from '.'; export const createCatchSchema = ( zodCatch: ZodCatch, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject | oas31.ReferenceObject => - createSchemaOrRef(zodCatch._def.innerType as ZodType, components); + createSchemaOrRef(zodCatch._def.innerType as ZodType, state); diff --git a/src/create/schema/default.test.ts b/src/create/schema/default.test.ts index d673622..9b59939 100644 --- a/src/create/schema/default.test.ts +++ b/src/create/schema/default.test.ts @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { createOutputState } from '../../testing/state'; import { createDefaultSchema } from './default'; @@ -16,7 +16,7 @@ describe('createDefaultSchema', () => { }; const schema = z.string().default('a'); - const result = createDefaultSchema(schema, getDefaultComponents()); + const result = createDefaultSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -34,7 +34,7 @@ describe('createDefaultSchema', () => { }; const schema = z.string().openapi({ ref: 'ref' }).optional().default('a'); - const result = createDefaultSchema(schema, getDefaultComponents()); + const result = createDefaultSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/default.ts b/src/create/schema/default.ts index 87d3019..2988c44 100644 --- a/src/create/schema/default.ts +++ b/src/create/schema/default.ts @@ -1,19 +1,17 @@ import { oas31 } from 'openapi3-ts'; import { ZodDefault, ZodTypeAny } from 'zod'; -import { ComponentsObject } from '../components'; - import { enhanceWithMetadata } from './metadata'; -import { createSchemaOrRef } from '.'; +import { SchemaState, createSchemaOrRef } from '.'; export const createDefaultSchema = ( zodDefault: ZodDefault, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject | oas31.ReferenceObject => { const schemaOrRef = createSchemaOrRef( zodDefault._def.innerType as ZodTypeAny, - components, + state, ); return enhanceWithMetadata(schemaOrRef, { diff --git a/src/create/schema/discriminatedUnion.test.ts b/src/create/schema/discriminatedUnion.test.ts index 34646d2..5dd3708 100644 --- a/src/create/schema/discriminatedUnion.test.ts +++ b/src/create/schema/discriminatedUnion.test.ts @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { createOutputState } from '../../testing/state'; import { createDiscriminatedUnionSchema } from './discriminatedUnion'; @@ -43,10 +43,7 @@ describe('createDiscriminatedUnionSchema', () => { }), ]); - const result = createDiscriminatedUnionSchema( - schema, - getDefaultComponents(), - ); + const result = createDiscriminatedUnionSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -78,10 +75,7 @@ describe('createDiscriminatedUnionSchema', () => { .openapi({ ref: 'b' }), ]); - const result = createDiscriminatedUnionSchema( - schema, - getDefaultComponents(), - ); + const result = createDiscriminatedUnionSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -114,10 +108,7 @@ describe('createDiscriminatedUnionSchema', () => { .openapi({ ref: 'd' }), ]); - const result = createDiscriminatedUnionSchema( - schema, - getDefaultComponents(), - ); + const result = createDiscriminatedUnionSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/discriminatedUnion.ts b/src/create/schema/discriminatedUnion.ts index 77461b4..02975f6 100644 --- a/src/create/schema/discriminatedUnion.ts +++ b/src/create/schema/discriminatedUnion.ts @@ -7,18 +7,16 @@ import { ZodRawShape, } from 'zod'; -import { ComponentsObject, createComponentSchemaRef } from '../components'; +import { createComponentSchemaRef } from '../components'; -import { createSchemaOrRef } from '.'; +import { SchemaState, createSchemaOrRef } from '.'; export const createDiscriminatedUnionSchema = ( zodDiscriminatedUnion: ZodDiscriminatedUnion, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject => { const options = zodDiscriminatedUnion.options as AnyZodObject[]; - const schemas = options.map((option) => - createSchemaOrRef(option, components), - ); + const schemas = options.map((option) => createSchemaOrRef(option, state)); const discriminator = mapDiscriminator( options, zodDiscriminatedUnion.discriminator as string, diff --git a/src/create/schema/effects.test.ts b/src/create/schema/effects.test.ts deleted file mode 100644 index ca7add7..0000000 --- a/src/create/schema/effects.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { oas31 } from 'openapi3-ts'; -import { z } from 'zod'; - -import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; - -import { createEffectsSchema } from './effects'; - -extendZodWithOpenApi(z); - -describe('createEffectsSchema', () => { - it('creates a schema from preprocess', () => { - const expected: oas31.SchemaObject = { - type: 'string', - }; - const schema = z.preprocess((arg) => String(arg), z.string()); - - const result = createEffectsSchema(schema, getDefaultComponents()); - - expect(result).toStrictEqual(expected); - }); - - it('creates a schema from refine', () => { - const expected: oas31.SchemaObject = { - type: 'string', - }; - const schema = z.string().refine((str) => { - str.startsWith('bla'); - }); - - const result = createEffectsSchema(schema, getDefaultComponents()); - - expect(result).toStrictEqual(expected); - }); -}); diff --git a/src/create/schema/effects.ts b/src/create/schema/effects.ts deleted file mode 100644 index 7316703..0000000 --- a/src/create/schema/effects.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { oas31 } from 'openapi3-ts'; -import { ZodEffects, ZodType } from 'zod'; - -import { ComponentsObject } from '../components'; - -import { createSchemaOrRef } from '.'; - -export const createEffectsSchema = ( - zodEffects: ZodEffects, - components: ComponentsObject, -): oas31.SchemaObject | oas31.ReferenceObject => - createSchemaOrRef(zodEffects._def.schema as ZodType, components); diff --git a/src/create/schema/index.test.ts b/src/create/schema/index.test.ts index fd6a655..955c23b 100644 --- a/src/create/schema/index.test.ts +++ b/src/create/schema/index.test.ts @@ -2,9 +2,10 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; +import { createInputState, createOutputState } from '../../testing/state'; import { getDefaultComponents } from '../components'; -import { createSchemaOrRef } from '.'; +import { SchemaState, createSchemaOrRef } from '.'; extendZodWithOpenApi(z); @@ -65,11 +66,6 @@ const expectedZodDiscriminatedUnion: oas31.SchemaObject = { ], }; -const zodEffect = z.preprocess((arg) => String(arg), z.string()); -const expectedZodEffect: oas31.SchemaObject = { - type: 'string', -}; - const zodEnum = z.enum(['a', 'b']); const expectedZodEnum: oas31.SchemaObject = { type: 'string', @@ -145,7 +141,7 @@ const expectedZodOptional: oas31.SchemaObject = { }; const zodRecord = z.record(z.string()); -const expectedZodReord: oas31.SchemaObject = { +const expectedZodRecord: oas31.SchemaObject = { type: 'object', additionalProperties: { type: 'string', @@ -189,33 +185,124 @@ const expectedZodCatch: oas31.SchemaObject = { type: 'string', }; +const zodPipeline = z + .string() + .transform((arg) => arg.length) + .pipe(z.number()); +const expectedZodPipelineOutput: oas31.SchemaObject = { + type: 'number', +}; +const expectedZodPipelineInput: oas31.SchemaObject = { + type: 'string', +}; + +const zodTransform = z.string().transform((str) => str.length); +const expectedZodTransform: oas31.SchemaObject = { + type: 'string', +}; + +const zodPreprocess = z.preprocess( + (arg) => (typeof arg === 'string' ? arg.split(',') : arg), + z.string(), +); +const expectedZodPreprocess: oas31.SchemaObject = { + type: 'string', +}; + +const zodRefine = z.string().refine((arg) => typeof arg === 'string'); +const expectedZodRefine: oas31.SchemaObject = { + type: 'string', +}; + +const zodUnknown = z.unknown().openapi({ type: 'string' }); +const expectedZodUnknown: oas31.SchemaObject = { + type: 'string', +}; + describe('createSchemaOrRef', () => { it.each` - zodType | schema | expected - ${'ZodArray'} | ${zodArray} | ${expectedZodArray} - ${'ZodBoolean'} | ${zodBoolean} | ${expectedZodBoolean} - ${'ZodDate'} | ${zodDate} | ${expectedZodDate} - ${'ZodDefault'} | ${zodDefault} | ${expectedZodDefault} - ${'ZodDiscriminatedUnion'} | ${zodDiscriminatedUnion} | ${expectedZodDiscriminatedUnion} - ${'ZodEffect'} | ${zodEffect} | ${expectedZodEffect} - ${'ZodEnum'} | ${zodEnum} | ${expectedZodEnum} - ${'ZodIntersection'} | ${zodIntersection} | ${expectedZodIntersection} - ${'ZodLiteral'} | ${zodLiteral} | ${expectedZodLiteral} - ${'ZodMetadata'} | ${zodMetadata} | ${expectedZodMetadata} - ${'ZodNativeEnum'} | ${zodNativeEnum} | ${expectedZodNativeEnum} - ${'ZodNull'} | ${zodNull} | ${expectedZodNull} - ${'ZodNullable'} | ${zodNullable} | ${expectedZodNullable} - ${'ZodNumber'} | ${zodNumber} | ${expectedZodNumber} - ${'ZodObject'} | ${zodObject} | ${expectedZodObject} - ${'ZodOptional'} | ${zodOptional} | ${expectedZodOptional} - ${'ZodRecord'} | ${zodRecord} | ${expectedZodReord} - ${'ZodString'} | ${zodString} | ${expectedZodString} - ${'ZodTuple'} | ${zodTuple} | ${expectedZodTuple} - ${'ZodUnion'} | ${zodUnion} | ${expectedZodUnion} - ${'ZodCatch'} | ${zodCatch} | ${expectedZodCatch} - `('creates a schema for $zodType', ({ schema, expected }) => { - expect(createSchemaOrRef(schema, getDefaultComponents())).toStrictEqual( + zodType | schema | expected + ${'ZodArray'} | ${zodArray} | ${expectedZodArray} + ${'ZodBoolean'} | ${zodBoolean} | ${expectedZodBoolean} + ${'ZodDate'} | ${zodDate} | ${expectedZodDate} + ${'ZodDefault'} | ${zodDefault} | ${expectedZodDefault} + ${'ZodDiscriminatedUnion'} | ${zodDiscriminatedUnion} | ${expectedZodDiscriminatedUnion} + ${'ZodEnum'} | ${zodEnum} | ${expectedZodEnum} + ${'ZodIntersection'} | ${zodIntersection} | ${expectedZodIntersection} + ${'ZodLiteral'} | ${zodLiteral} | ${expectedZodLiteral} + ${'ZodMetadata'} | ${zodMetadata} | ${expectedZodMetadata} + ${'ZodNativeEnum'} | ${zodNativeEnum} | ${expectedZodNativeEnum} + ${'ZodNull'} | ${zodNull} | ${expectedZodNull} + ${'ZodNullable'} | ${zodNullable} | ${expectedZodNullable} + ${'ZodNumber'} | ${zodNumber} | ${expectedZodNumber} + ${'ZodObject'} | ${zodObject} | ${expectedZodObject} + ${'ZodOptional'} | ${zodOptional} | ${expectedZodOptional} + ${'ZodRecord'} | ${zodRecord} | ${expectedZodRecord} + ${'ZodString'} | ${zodString} | ${expectedZodString} + ${'ZodTuple'} | ${zodTuple} | ${expectedZodTuple} + ${'ZodUnion'} | ${zodUnion} | ${expectedZodUnion} + ${'ZodCatch'} | ${zodCatch} | ${expectedZodCatch} + ${'ZodPipeline'} | ${zodPipeline} | ${expectedZodPipelineOutput} + ${'ZodEffects - Preprocess'} | ${zodPreprocess} | ${expectedZodPreprocess} + ${'ZodEffects - Refine'} | ${zodRefine} | ${expectedZodRefine} + ${'unknown'} | ${zodUnknown} | ${expectedZodUnknown} + `('creates an output schema for $zodType', ({ schema, expected }) => { + expect(createSchemaOrRef(schema, createOutputState())).toStrictEqual( expected, ); }); + + it.each` + zodType | schema | expected + ${'ZodArray'} | ${zodArray} | ${expectedZodArray} + ${'ZodBoolean'} | ${zodBoolean} | ${expectedZodBoolean} + ${'ZodDate'} | ${zodDate} | ${expectedZodDate} + ${'ZodDefault'} | ${zodDefault} | ${expectedZodDefault} + ${'ZodDiscriminatedUnion'} | ${zodDiscriminatedUnion} | ${expectedZodDiscriminatedUnion} + ${'ZodEnum'} | ${zodEnum} | ${expectedZodEnum} + ${'ZodIntersection'} | ${zodIntersection} | ${expectedZodIntersection} + ${'ZodLiteral'} | ${zodLiteral} | ${expectedZodLiteral} + ${'ZodMetadata'} | ${zodMetadata} | ${expectedZodMetadata} + ${'ZodNativeEnum'} | ${zodNativeEnum} | ${expectedZodNativeEnum} + ${'ZodNull'} | ${zodNull} | ${expectedZodNull} + ${'ZodNullable'} | ${zodNullable} | ${expectedZodNullable} + ${'ZodNumber'} | ${zodNumber} | ${expectedZodNumber} + ${'ZodObject'} | ${zodObject} | ${expectedZodObject} + ${'ZodOptional'} | ${zodOptional} | ${expectedZodOptional} + ${'ZodRecord'} | ${zodRecord} | ${expectedZodRecord} + ${'ZodString'} | ${zodString} | ${expectedZodString} + ${'ZodTuple'} | ${zodTuple} | ${expectedZodTuple} + ${'ZodUnion'} | ${zodUnion} | ${expectedZodUnion} + ${'ZodCatch'} | ${zodCatch} | ${expectedZodCatch} + ${'ZodPipeline'} | ${zodPipeline} | ${expectedZodPipelineInput} + ${'ZodEffects - Transform'} | ${zodTransform} | ${expectedZodTransform} + ${'ZodEffects - Refine'} | ${zodRefine} | ${expectedZodRefine} + ${'unknown'} | ${zodUnknown} | ${expectedZodUnknown} + `('creates an input schema for $zodType', ({ schema, expected }) => { + expect(createSchemaOrRef(schema, createInputState())).toStrictEqual( + expected, + ); + }); + + it('throws an error when an ZodEffect input component is referenced in an output', () => { + const inputSchema = z + .object({ a: z.string().transform((arg) => arg.length) }) + .openapi({ ref: 'a' }); + const components = getDefaultComponents(); + const state: SchemaState = { + components, + type: 'input', + }; + createSchemaOrRef(inputSchema, state); + + const outputState: SchemaState = { + components, + type: 'output', + }; + + const outputSchema = z.object({ a: inputSchema }); + expect(() => createSchemaOrRef(outputSchema, outputState)).toThrow( + 'schemaRef "a" was created with a ZodEffect meaning that the input type is different from the output type. This type is currently being referenced in a response and request. Wrap the ZodEffect in a ZodPipeline to verify the contents of the effect', + ); + }); }); diff --git a/src/create/schema/index.ts b/src/create/schema/index.ts index d3d3571..bdeb52f 100644 --- a/src/create/schema/index.ts +++ b/src/create/schema/index.ts @@ -16,6 +16,7 @@ import { ZodNumber, ZodObject, ZodOptional, + ZodPipeline, ZodRecord, ZodString, ZodTuple, @@ -24,7 +25,11 @@ import { ZodUnion, } from 'zod'; -import { ComponentsObject, createComponentSchemaRef } from '../components'; +import { + ComponentsObject, + CreationType, + createComponentSchemaRef, +} from '../components'; import { createArraySchema } from './array'; import { createBooleanSchema } from './boolean'; @@ -32,7 +37,6 @@ import { createCatchSchema } from './catch'; import { createDateSchema } from './date'; import { createDefaultSchema } from './default'; import { createDiscriminatedUnionSchema } from './discriminatedUnion'; -import { createEffectsSchema } from './effects'; import { createEnumSchema } from './enum'; import { createIntersectionSchema } from './intersection'; import { createLiteralSchema } from './literal'; @@ -43,10 +47,21 @@ import { createNullableSchema } from './nullable'; import { createNumberSchema } from './number'; import { createObjectSchema } from './object'; import { createOptionalSchema } from './optional'; +import { createPipelineSchema } from './pipeline'; +import { createPreprocessSchema } from './preprocess'; import { createRecordSchema } from './record'; +import { createRefineSchema } from './refine'; import { createStringSchema } from './string'; +import { createTransformSchema } from './transform'; import { createTupleSchema } from './tuple'; import { createUnionSchema } from './union'; +import { createUnknownSchema } from './unknown'; + +export interface SchemaState { + components: ComponentsObject; + type: CreationType; + effectType?: CreationType; +} export const createSchema = < Output = any, @@ -54,14 +69,14 @@ export const createSchema = < Input = Output, >( zodSchema: ZodType, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject | oas31.ReferenceObject => { if (zodSchema instanceof ZodString) { return createStringSchema(zodSchema); } if (zodSchema instanceof ZodNumber) { - return createNumberSchema(zodSchema, components); + return createNumberSchema(zodSchema, state); } if (zodSchema instanceof ZodBoolean) { @@ -77,23 +92,23 @@ export const createSchema = < } if (zodSchema instanceof ZodNativeEnum) { - return createNativeEnumSchema(zodSchema, components); + return createNativeEnumSchema(zodSchema, state); } if (zodSchema instanceof ZodArray) { - return createArraySchema(zodSchema, components); + return createArraySchema(zodSchema, state); } if (zodSchema instanceof ZodObject) { - return createObjectSchema(zodSchema, components); + return createObjectSchema(zodSchema, state); } if (zodSchema instanceof ZodUnion) { - return createUnionSchema(zodSchema, components); + return createUnionSchema(zodSchema, state); } if (zodSchema instanceof ZodDiscriminatedUnion) { - return createDiscriminatedUnionSchema(zodSchema, components); + return createDiscriminatedUnionSchema(zodSchema, state); } if (zodSchema instanceof ZodNull) { @@ -101,56 +116,67 @@ export const createSchema = < } if (zodSchema instanceof ZodNullable) { - return createNullableSchema(zodSchema, components); + return createNullableSchema(zodSchema, state); } if (zodSchema instanceof ZodOptional) { - return createOptionalSchema(zodSchema, components); + return createOptionalSchema(zodSchema, state); } if (zodSchema instanceof ZodDefault) { - return createDefaultSchema(zodSchema, components); + return createDefaultSchema(zodSchema, state); } if (zodSchema instanceof ZodRecord) { - return createRecordSchema(zodSchema, components); + return createRecordSchema(zodSchema, state); } if (zodSchema instanceof ZodTuple) { - return createTupleSchema(zodSchema, components); + return createTupleSchema(zodSchema, state); } if (zodSchema instanceof ZodDate) { return createDateSchema(zodSchema); } + if (zodSchema instanceof ZodPipeline) { + return createPipelineSchema(zodSchema, state); + } + if ( zodSchema instanceof ZodEffects && - (zodSchema._def.effect.type === 'refinement' || - zodSchema._def.effect.type === 'preprocess') + zodSchema._def.effect.type === 'transform' ) { - return createEffectsSchema(zodSchema, components); + return createTransformSchema(zodSchema, state); + } + + if ( + zodSchema instanceof ZodEffects && + zodSchema._def.effect.type === 'preprocess' + ) { + return createPreprocessSchema(zodSchema, state); + } + + if ( + zodSchema instanceof ZodEffects && + zodSchema._def.effect.type === 'refinement' + ) { + return createRefineSchema(zodSchema, state); } if (zodSchema instanceof ZodNativeEnum) { - return createNativeEnumSchema(zodSchema, components); + return createNativeEnumSchema(zodSchema, state); } if (zodSchema instanceof ZodIntersection) { - return createIntersectionSchema(zodSchema, components); + return createIntersectionSchema(zodSchema, state); } if (zodSchema instanceof ZodCatch) { - return createCatchSchema(zodSchema, components); - } - - if (!zodSchema._def.openapi?.type) { - throw new Error( - `Unknown schema ${zodSchema.toString()}. Please assign it a manual type`, - ); + return createCatchSchema(zodSchema, state); } - return {}; + return createUnknownSchema(zodSchema); }; export const createRegisteredSchema = < @@ -160,29 +186,44 @@ export const createRegisteredSchema = < >( zodSchema: ZodType, schemaRef: string, - components: ComponentsObject, + state: SchemaState, ): oas31.ReferenceObject => { - const component = components.schemas[schemaRef]; + const component = state.components.schemas[schemaRef]; if (component) { if (component.zodSchema !== zodSchema) { throw new Error(`schemaRef "${schemaRef}" is already registered`); } + if (!component.types?.includes(state.type)) { + throw new Error( + `schemaRef "${schemaRef}" was created with a ZodEffect meaning that the input type is different from the output type. This type is currently being referenced in a response and request. Wrap the ZodEffect in a ZodPipeline to verify the contents of the effect`, + ); + } return { $ref: createComponentSchemaRef(schemaRef), }; } + const newState: SchemaState = { + components: state.components, + type: state.type, + }; + + const schemaOrRef = createSchemaWithMetadata(zodSchema, newState); // Optional Objects can return a reference object - const schemaOrRef = createSchemaWithMetadata(zodSchema, components); if ('$ref' in schemaOrRef) { throw new Error('Unexpected Error: received a reference object'); } - components.schemas[schemaRef] = { + state.components.schemas[schemaRef] = { schemaObject: schemaOrRef, zodSchema, + types: newState?.effectType ? [newState.effectType] : ['input', 'output'], }; + if (newState.effectType) { + state.effectType = newState.effectType; + } + return { $ref: createComponentSchemaRef(schemaRef), }; @@ -194,12 +235,12 @@ export const createSchemaOrRef = < Input = Output, >( zodSchema: ZodType, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject | oas31.ReferenceObject => { const schemaRef = zodSchema._def.openapi?.ref; if (schemaRef) { - return createRegisteredSchema(zodSchema, schemaRef, components); + return createRegisteredSchema(zodSchema, schemaRef, state); } - return createSchemaWithMetadata(zodSchema, components); + return createSchemaWithMetadata(zodSchema, state); }; diff --git a/src/create/schema/intersection.test.ts b/src/create/schema/intersection.test.ts index fdac2cb..c248c3b 100644 --- a/src/create/schema/intersection.test.ts +++ b/src/create/schema/intersection.test.ts @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { createOutputState } from '../../testing/state'; import { createIntersectionSchema } from './intersection'; @@ -22,7 +22,7 @@ describe('createIntersectionSchema', () => { }; const schema = z.intersection(z.string(), z.number()); - const result = createIntersectionSchema(schema, getDefaultComponents()); + const result = createIntersectionSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/intersection.ts b/src/create/schema/intersection.ts index 7f7ed63..d9c741c 100644 --- a/src/create/schema/intersection.ts +++ b/src/create/schema/intersection.ts @@ -1,16 +1,14 @@ import { oas31 } from 'openapi3-ts'; import { ZodIntersection, ZodType } from 'zod'; -import { ComponentsObject } from '../components'; - -import { createSchemaOrRef } from '.'; +import { SchemaState, createSchemaOrRef } from '.'; export const createIntersectionSchema = ( zodIntersection: ZodIntersection, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject | oas31.ReferenceObject => ({ allOf: [ - createSchemaOrRef(zodIntersection._def.left as ZodType, components), - createSchemaOrRef(zodIntersection._def.right as ZodType, components), + createSchemaOrRef(zodIntersection._def.left as ZodType, state), + createSchemaOrRef(zodIntersection._def.right as ZodType, state), ], }); diff --git a/src/create/schema/metadata.test.ts b/src/create/schema/metadata.test.ts index 819e764..9fbd3f0 100644 --- a/src/create/schema/metadata.test.ts +++ b/src/create/schema/metadata.test.ts @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { createOutputState } from '../../testing/state'; import { createSchemaWithMetadata } from './metadata'; @@ -16,7 +16,7 @@ describe('createSchemaWithMetadata', () => { }; const schema = z.string().openapi({ description: 'bla' }); - const result = createSchemaWithMetadata(schema, getDefaultComponents()); + const result = createSchemaWithMetadata(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -28,7 +28,7 @@ describe('createSchemaWithMetadata', () => { }; const schema = z.string().describe('bla'); - const result = createSchemaWithMetadata(schema, getDefaultComponents()); + const result = createSchemaWithMetadata(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -41,7 +41,7 @@ describe('createSchemaWithMetadata', () => { const schema = z.string().describe('bla').openapi({ description: 'foo' }); - const result = createSchemaWithMetadata(schema, getDefaultComponents()); + const result = createSchemaWithMetadata(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -52,7 +52,7 @@ describe('createSchemaWithMetadata', () => { }; const schema = z.string().openapi({ type: 'integer' }); - const result = createSchemaWithMetadata(schema, getDefaultComponents()); + const result = createSchemaWithMetadata(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -73,7 +73,7 @@ describe('createSchemaWithMetadata', () => { const schema = ref.optional().openapi({ description: 'hello' }); - const result = createSchemaWithMetadata(schema, getDefaultComponents()); + const result = createSchemaWithMetadata(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -87,7 +87,7 @@ describe('createSchemaWithMetadata', () => { const schema = ref.optional(); - const result = createSchemaWithMetadata(schema, getDefaultComponents()); + const result = createSchemaWithMetadata(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -100,7 +100,7 @@ describe('createSchemaWithMetadata', () => { const ref = z.string().openapi({ ref: 'ref2' }); const schema = ref.optional().default('a'); - const result = createSchemaWithMetadata(schema, getDefaultComponents()); + const result = createSchemaWithMetadata(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -129,7 +129,7 @@ describe('createSchemaWithMetadata', () => { b: object2.openapi({ description: 'jello' }), }); - const result = createSchemaWithMetadata(schema, getDefaultComponents()); + const result = createSchemaWithMetadata(schema, createOutputState()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/metadata.ts b/src/create/schema/metadata.ts index 821ba00..3e378e1 100644 --- a/src/create/schema/metadata.ts +++ b/src/create/schema/metadata.ts @@ -1,9 +1,7 @@ import { oas31 } from 'openapi3-ts'; import { ZodType, ZodTypeDef } from 'zod'; -import { ComponentsObject } from '../components'; - -import { createSchema } from '.'; +import { SchemaState, createSchema } from '.'; export const createSchemaWithMetadata = < Output = any, @@ -11,11 +9,11 @@ export const createSchemaWithMetadata = < Input = Output, >( zodSchema: ZodType, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject | oas31.ReferenceObject => { - const { ref, param, header, ...additionalMetadata } = + const { ref, refType, param, header, ...additionalMetadata } = zodSchema._def.openapi ?? {}; - const schemaOrRef = createSchema(zodSchema, components); + const schemaOrRef = createSchema(zodSchema, state); const description = zodSchema.description; return enhanceWithMetadata(schemaOrRef, { diff --git a/src/create/schema/nativeEnum.test.ts b/src/create/schema/nativeEnum.test.ts index 3eec3c5..104e16c 100644 --- a/src/create/schema/nativeEnum.test.ts +++ b/src/create/schema/nativeEnum.test.ts @@ -2,7 +2,10 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { + createOutputOpenapi3State, + createOutputState, +} from '../../testing/state'; import { createNativeEnumSchema } from './nativeEnum'; @@ -24,7 +27,7 @@ describe('createNativeEnumSchema', () => { const schema = z.nativeEnum(Direction); - const result = createNativeEnumSchema(schema, getDefaultComponents()); + const result = createNativeEnumSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -43,7 +46,7 @@ describe('createNativeEnumSchema', () => { } const schema = z.nativeEnum(Direction); - const result = createNativeEnumSchema(schema, getDefaultComponents()); + const result = createNativeEnumSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -63,7 +66,7 @@ describe('createNativeEnumSchema', () => { const schema = z.nativeEnum(Direction); - const result = createNativeEnumSchema(schema, getDefaultComponents()); + const result = createNativeEnumSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -91,10 +94,7 @@ describe('createNativeEnumSchema', () => { const schema = z.nativeEnum(Direction); - const result = createNativeEnumSchema( - schema, - getDefaultComponents({}, '3.0.0'), - ); + const result = createNativeEnumSchema(schema, createOutputOpenapi3State()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/nativeEnum.ts b/src/create/schema/nativeEnum.ts index eb5ad66..f8a6519 100644 --- a/src/create/schema/nativeEnum.ts +++ b/src/create/schema/nativeEnum.ts @@ -2,17 +2,18 @@ import { oas31 } from 'openapi3-ts'; import { EnumLike, ZodNativeEnum } from 'zod'; import { satisfiesVersion } from '../../openapi'; -import { ComponentsObject } from '../components'; + +import { SchemaState } from '.'; export const createNativeEnumSchema = ( zodEnum: ZodNativeEnum, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject | oas31.ReferenceObject => { const enumValues = getValidEnumValues(zodEnum._def.values); const { numbers, strings } = sortStringsAndNumbers(enumValues); if (strings.length && numbers.length) { - if (satisfiesVersion(components.openapi, '3.1.0')) + if (satisfiesVersion(state.components.openapi, '3.1.0')) return { type: ['string', 'number'], enum: [...strings, ...numbers], diff --git a/src/create/schema/nullable.test.ts b/src/create/schema/nullable.test.ts index ca2951c..5e9702a 100644 --- a/src/create/schema/nullable.test.ts +++ b/src/create/schema/nullable.test.ts @@ -2,7 +2,10 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { + createOutputOpenapi3State, + createOutputState, +} from '../../testing/state'; import { createNullableSchema } from './nullable'; @@ -17,10 +20,7 @@ describe('createNullableSchema', () => { }; const schema = z.string().nullable(); - const result = createNullableSchema( - schema, - getDefaultComponents({}, '3.0.0'), - ); + const result = createNullableSchema(schema, createOutputOpenapi3State()); expect(result).toStrictEqual(expected); }); @@ -39,10 +39,7 @@ describe('createNullableSchema', () => { const registered = z.string().openapi({ ref: 'a' }); const schema = registered.optional().nullable(); - const result = createNullableSchema( - schema, - getDefaultComponents({}, '3.0.0'), - ); + const result = createNullableSchema(schema, createOutputOpenapi3State()); expect(result).toStrictEqual(expected); }); @@ -77,10 +74,7 @@ describe('createNullableSchema', () => { .union([z.object({ a: z.string() }), z.object({ b: z.string() })]) .nullable(); - const result = createNullableSchema( - schema, - getDefaultComponents({}, '3.0.0'), - ); + const result = createNullableSchema(schema, createOutputOpenapi3State()); expect(result).toStrictEqual(expected); }); @@ -112,10 +106,7 @@ describe('createNullableSchema', () => { const object2 = object1.extend({ b: z.string() }); const schema = z.object({ b: object2.nullable() }).nullable(); - const result = createNullableSchema( - schema, - getDefaultComponents({}, '3.0.0'), - ); + const result = createNullableSchema(schema, createOutputOpenapi3State()); expect(result).toStrictEqual(expected); }); @@ -128,7 +119,7 @@ describe('createNullableSchema', () => { }; const schema = z.string().nullable(); - const result = createNullableSchema(schema, getDefaultComponents()); + const result = createNullableSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -147,7 +138,7 @@ describe('createNullableSchema', () => { const registered = z.string().openapi({ ref: 'a' }); const schema = registered.optional().nullable(); - const result = createNullableSchema(schema, getDefaultComponents()); + const result = createNullableSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -182,7 +173,7 @@ describe('createNullableSchema', () => { .union([z.object({ a: z.string() }), z.object({ b: z.string() })]) .nullable(); - const result = createNullableSchema(schema, getDefaultComponents()); + const result = createNullableSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -213,7 +204,7 @@ describe('createNullableSchema', () => { const object2 = object1.extend({ b: z.string() }); const schema = z.object({ b: object2.nullable() }).nullable(); - const result = createNullableSchema(schema, getDefaultComponents()); + const result = createNullableSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/nullable.ts b/src/create/schema/nullable.ts index 360dd09..98cd5e1 100644 --- a/src/create/schema/nullable.ts +++ b/src/create/schema/nullable.ts @@ -2,30 +2,29 @@ import { oas31 } from 'openapi3-ts'; import { ZodNullable, ZodTypeAny } from 'zod'; import { satisfiesVersion } from '../../openapi'; -import { ComponentsObject } from '../components'; import { ZodOpenApiVersion } from '../document'; -import { createSchemaOrRef } from '.'; +import { SchemaState, createSchemaOrRef } from '.'; export const createNullableSchema = ( zodNullable: ZodNullable, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject => { const schemaOrReference = createSchemaOrRef( zodNullable.unwrap() as ZodTypeAny, - components, + state, ); if ('$ref' in schemaOrReference || schemaOrReference.allOf) { return { - oneOf: mapNullOf([schemaOrReference], components.openapi), + oneOf: mapNullOf([schemaOrReference], state.components.openapi), }; } if (schemaOrReference.oneOf) { const { oneOf, ...schema } = schemaOrReference; return { - oneOf: mapNullOf(oneOf, components.openapi), + oneOf: mapNullOf(oneOf, state.components.openapi), ...schema, }; } @@ -33,14 +32,14 @@ export const createNullableSchema = ( if (schemaOrReference.anyOf) { const { anyOf, ...schema } = schemaOrReference; return { - anyOf: mapNullOf(anyOf, components.openapi), + anyOf: mapNullOf(anyOf, state.components.openapi), ...schema, }; } const { type, ...schema } = schemaOrReference; - if (satisfiesVersion(components.openapi, '3.1.0')) { + if (satisfiesVersion(state.components.openapi, '3.1.0')) { return { type: mapNullType(type), ...schema, diff --git a/src/create/schema/number.test.ts b/src/create/schema/number.test.ts index 2a04996..87c8ead 100644 --- a/src/create/schema/number.test.ts +++ b/src/create/schema/number.test.ts @@ -2,7 +2,10 @@ import { oas30, oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { + createOutputOpenapi3State, + createOutputState, +} from '../../testing/state'; import { createNumberSchema } from './number'; @@ -15,7 +18,7 @@ describe('createNumberSchema', () => { }; const schema = z.number(); - const result = createNumberSchema(schema, getDefaultComponents()); + const result = createNumberSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -26,7 +29,7 @@ describe('createNumberSchema', () => { }; const schema = z.number().int(); - const result = createNumberSchema(schema, getDefaultComponents()); + const result = createNumberSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -39,7 +42,7 @@ describe('createNumberSchema', () => { }; const schema = z.number().lt(10).gt(0); - const result = createNumberSchema(schema, getDefaultComponents()); + const result = createNumberSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -52,7 +55,7 @@ describe('createNumberSchema', () => { }; const schema = z.number().lte(10).gte(0); - const result = createNumberSchema(schema, getDefaultComponents()); + const result = createNumberSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -65,10 +68,7 @@ describe('createNumberSchema', () => { }; const schema = z.number().lte(10).gte(0); - const result = createNumberSchema( - schema, - getDefaultComponents({}, '3.0.0'), - ); + const result = createNumberSchema(schema, createOutputOpenapi3State()); expect(result).toStrictEqual(expected); }); @@ -83,10 +83,7 @@ describe('createNumberSchema', () => { }; const schema = z.number().lt(10).gt(0); - const result = createNumberSchema( - schema, - getDefaultComponents({}, '3.0.0'), - ); + const result = createNumberSchema(schema, createOutputOpenapi3State()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/number.ts b/src/create/schema/number.ts index 5d61951..b15359d 100644 --- a/src/create/schema/number.ts +++ b/src/create/schema/number.ts @@ -2,17 +2,18 @@ import { oas30, oas31 } from 'openapi3-ts'; import { ZodNumber, ZodNumberCheck } from 'zod'; import { satisfiesVersion } from '../../openapi'; -import { ComponentsObject } from '../components'; import { ZodOpenApiVersion } from '../document'; +import { SchemaState } from '.'; + export const createNumberSchema = ( zodNumber: ZodNumber, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject => { const zodNumberChecks = getZodNumberChecks(zodNumber); - const minimum = mapMinimum(zodNumberChecks, components.openapi); - const maximum = mapMaximum(zodNumberChecks, components.openapi); + const minimum = mapMinimum(zodNumberChecks, state.components.openapi); + const maximum = mapMaximum(zodNumberChecks, state.components.openapi); return { type: mapNumberType(zodNumberChecks), diff --git a/src/create/schema/object.test.ts b/src/create/schema/object.test.ts index 1055389..284d06e 100644 --- a/src/create/schema/object.test.ts +++ b/src/create/schema/object.test.ts @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { createOutputState } from '../../testing/state'; import { createObjectSchema } from './object'; @@ -23,7 +23,7 @@ describe('createObjectSchema', () => { b: z.string().optional(), }); - const result = createObjectSchema(schema, getDefaultComponents()); + const result = createObjectSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -43,7 +43,7 @@ describe('createObjectSchema', () => { a: z.string(), }); - const result = createObjectSchema(schema, getDefaultComponents()); + const result = createObjectSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -73,7 +73,7 @@ describe('createObjectSchema', () => { obj2: object2, }); - const result = createObjectSchema(schema, getDefaultComponents()); + const result = createObjectSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/object.ts b/src/create/schema/object.ts index 4becfab..1794aac 100644 --- a/src/create/schema/object.ts +++ b/src/create/schema/object.ts @@ -1,30 +1,30 @@ import { oas31 } from 'openapi3-ts'; import { UnknownKeysParam, ZodObject, ZodRawShape } from 'zod'; -import { ComponentsObject, createComponentSchemaRef } from '../components'; +import { createComponentSchemaRef } from '../components'; -import { createSchemaOrRef } from '.'; +import { SchemaState, createSchemaOrRef } from '.'; export const createObjectSchema = < T extends ZodRawShape, UnknownKeys extends UnknownKeysParam = UnknownKeysParam, >( zodObject: ZodObject, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject => { if (zodObject._def.extendMetadata?.extendsRef) { return createExtendedSchema( zodObject, zodObject._def.extendMetadata.extends, zodObject._def.extendMetadata.extendsRef, - components, + state, ); } return createObjectSchemaFromShape( zodObject.shape, zodObject._def.unknownKeys === 'strict', - components, + state, ); }; @@ -32,7 +32,7 @@ export const createExtendedSchema = ( zodObject: ZodObject, baseZodObject: ZodObject, schemaRef: string, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject => { const diffShape = createShapeDiff( baseZodObject._def.shape() as ZodRawShape, @@ -42,7 +42,7 @@ export const createExtendedSchema = ( return { allOf: [ { $ref: createComponentSchemaRef(schemaRef) }, - createObjectSchemaFromShape(diffShape, false, components), + createObjectSchemaFromShape(diffShape, false, state), ], }; }; @@ -62,10 +62,10 @@ const createShapeDiff = ( export const createObjectSchemaFromShape = ( shape: ZodRawShape, strict: boolean, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject => ({ type: 'object', - properties: mapProperties(shape, components), + properties: mapProperties(shape, state), required: mapRequired(shape), ...(strict && { additionalProperties: false }), }); @@ -86,11 +86,11 @@ export const mapRequired = ( export const mapProperties = ( shape: ZodRawShape, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject['properties'] => Object.entries(shape).reduce>( (acc, [key, zodSchema]): NonNullable => { - acc[key] = createSchemaOrRef(zodSchema, components); + acc[key] = createSchemaOrRef(zodSchema, state); return acc; }, {}, diff --git a/src/create/schema/optional.test.ts b/src/create/schema/optional.test.ts index 1d4d0f0..307d0ad 100644 --- a/src/create/schema/optional.test.ts +++ b/src/create/schema/optional.test.ts @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { createOutputState } from '../../testing/state'; import { createOptionalSchema } from './optional'; @@ -15,7 +15,7 @@ describe('createOptionalSchema', () => { }; const schema = z.string().optional(); - const result = createOptionalSchema(schema, getDefaultComponents()); + const result = createOptionalSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/optional.ts b/src/create/schema/optional.ts index bb65e39..eeb7e51 100644 --- a/src/create/schema/optional.ts +++ b/src/create/schema/optional.ts @@ -1,13 +1,11 @@ import { oas31 } from 'openapi3-ts'; import { ZodOptional, ZodTypeAny } from 'zod'; -import { ComponentsObject } from '../components'; - -import { createSchemaOrRef } from '.'; +import { SchemaState, createSchemaOrRef } from '.'; export const createOptionalSchema = ( zodOptional: ZodOptional, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject | oas31.ReferenceObject => // Optional doesn't change OpenAPI schema - createSchemaOrRef(zodOptional.unwrap() as ZodTypeAny, components); + createSchemaOrRef(zodOptional.unwrap() as ZodTypeAny, state); diff --git a/src/create/schema/pipeline.test.ts b/src/create/schema/pipeline.test.ts new file mode 100644 index 0000000..71d5ae4 --- /dev/null +++ b/src/create/schema/pipeline.test.ts @@ -0,0 +1,65 @@ +import { oas31 } from 'openapi3-ts'; +import { z } from 'zod'; + +import { extendZodWithOpenApi } from '../../extendZod'; +import { createInputState, createOutputState } from '../../testing/state'; + +import { createPipelineSchema } from './pipeline'; + +extendZodWithOpenApi(z); + +describe('createTransformSchema', () => { + describe('input', () => { + it('creates a schema from a simple pipeline', () => { + const expected: oas31.SchemaObject = { + type: 'string', + }; + const schema = z.string().pipe(z.string()); + + const result = createPipelineSchema(schema, createInputState()); + + expect(result).toStrictEqual(expected); + }); + + it('creates a schema from a transform pipeline', () => { + const expected: oas31.SchemaObject = { + type: 'string', + }; + const schema = z + .string() + .transform((arg) => arg.length) + .pipe(z.number()); + + const result = createPipelineSchema(schema, createInputState()); + + expect(result).toStrictEqual(expected); + }); + }); + + describe('output', () => { + it('creates a schema from a simple pipeline', () => { + const expected: oas31.SchemaObject = { + type: 'string', + }; + const schema = z.string().pipe(z.string()); + + const result = createPipelineSchema(schema, createOutputState()); + + expect(result).toStrictEqual(expected); + }); + + it('creates a schema from a transform pipeline', () => { + const expected: oas31.SchemaObject = { + type: 'number', + }; + const schema = z + .string() + .transform((arg) => arg.length) + .pipe(z.number()); + + const result = createPipelineSchema(schema, createOutputState()); + + expect(result).toStrictEqual(expected); + }); + }); +}); diff --git a/src/create/schema/pipeline.ts b/src/create/schema/pipeline.ts new file mode 100644 index 0000000..efa2c59 --- /dev/null +++ b/src/create/schema/pipeline.ts @@ -0,0 +1,14 @@ +import { oas31 } from 'openapi3-ts'; +import { ZodPipeline, ZodTypeAny } from 'zod'; + +import { SchemaState, createSchemaOrRef } from '.'; + +export const createPipelineSchema = ( + zodPipeline: ZodPipeline, + state: SchemaState, +): oas31.SchemaObject | oas31.ReferenceObject => { + if (state.type === 'input') { + return createSchemaOrRef(zodPipeline._def.in as ZodTypeAny, state); + } + return createSchemaOrRef(zodPipeline._def.out as ZodTypeAny, state); +}; diff --git a/src/create/schema/preprocess.test.ts b/src/create/schema/preprocess.test.ts new file mode 100644 index 0000000..7dc155b --- /dev/null +++ b/src/create/schema/preprocess.test.ts @@ -0,0 +1,72 @@ +import { oas31 } from 'openapi3-ts'; +import { z } from 'zod'; + +import { extendZodWithOpenApi } from '../../extendZod'; +import { createInputState, createOutputState } from '../../testing/state'; + +import { createPreprocessSchema } from './preprocess'; + +extendZodWithOpenApi(z); + +describe('createPreprocessSchema', () => { + describe('input', () => { + it('throws an error when creating an input schema with preprocess', () => { + const schema = z.preprocess( + (arg) => (typeof arg === 'string' ? arg.split(',') : arg), + z.string(), + ); + expect(() => + createPreprocessSchema(schema, createInputState()), + ).toThrow(); + }); + + it('returns a manually declared type when creating an input schema with preprocess', () => { + const expected: oas31.SchemaObject = { + type: 'string', + }; + const schema = z + .preprocess( + (arg) => (typeof arg === 'string' ? arg.split(',') : arg), + z.string(), + ) + .openapi({ type: 'string' }); + + const result = createPreprocessSchema(schema, createInputState()); + + expect(result).toStrictEqual(expected); + }); + }); + + describe('output', () => { + it('returns a schema when creating an output schema with preprocess', () => { + const expected: oas31.SchemaObject = { + type: 'string', + }; + const schema = z + .preprocess( + (arg) => (typeof arg === 'string' ? arg.split(',') : arg), + z.string(), + ) + .openapi({ type: 'string' }); + + const result = createPreprocessSchema(schema, createOutputState()); + + expect(result).toStrictEqual(expected); + }); + + it('changes the state effectType to output', () => { + const schema = z + .preprocess( + (arg) => (typeof arg === 'string' ? arg.split(',') : arg), + z.string(), + ) + .openapi({ type: 'string' }); + + const state = createOutputState(); + + createPreprocessSchema(schema, state); + + expect(state.effectType).toBe('output'); + }); + }); +}); diff --git a/src/create/schema/preprocess.ts b/src/create/schema/preprocess.ts new file mode 100644 index 0000000..195c4f1 --- /dev/null +++ b/src/create/schema/preprocess.ts @@ -0,0 +1,18 @@ +import { oas31 } from 'openapi3-ts'; +import { ZodEffects, ZodType } from 'zod'; + +import { createUnknownSchema } from './unknown'; + +import { SchemaState, createSchemaOrRef } from '.'; + +export const createPreprocessSchema = ( + zodPreprocess: ZodEffects, + state: SchemaState, +): oas31.SchemaObject | oas31.ReferenceObject => { + if (state.type === 'output') { + state.effectType = 'output'; + return createSchemaOrRef(zodPreprocess._def.schema as ZodType, state); + } + + return createUnknownSchema(zodPreprocess); +}; diff --git a/src/create/schema/record.test.ts b/src/create/schema/record.test.ts index 94af6a0..98edcb8 100644 --- a/src/create/schema/record.test.ts +++ b/src/create/schema/record.test.ts @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { createOutputState } from '../../testing/state'; import { createRecordSchema } from './record'; @@ -18,7 +18,7 @@ describe('createRecordSchema', () => { }; const schema = z.record(z.string()); - const result = createRecordSchema(schema, getDefaultComponents()); + const result = createRecordSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/record.ts b/src/create/schema/record.ts index c7cf2d5..29dad6f 100644 --- a/src/create/schema/record.ts +++ b/src/create/schema/record.ts @@ -1,17 +1,15 @@ import { oas31 } from 'openapi3-ts'; import { ZodRecord, ZodTypeAny } from 'zod'; -import { ComponentsObject } from '../components'; - -import { createSchemaOrRef } from '.'; +import { SchemaState, createSchemaOrRef } from '.'; export const createRecordSchema = ( zodRecord: ZodRecord, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject => ({ type: 'object', additionalProperties: createSchemaOrRef( zodRecord.valueSchema as ZodTypeAny, - components, + state, ), }); diff --git a/src/create/schema/refine.test.ts b/src/create/schema/refine.test.ts new file mode 100644 index 0000000..c7a324c --- /dev/null +++ b/src/create/schema/refine.test.ts @@ -0,0 +1,22 @@ +import { oas31 } from 'openapi3-ts'; +import { z } from 'zod'; + +import { extendZodWithOpenApi } from '../../extendZod'; +import { createOutputState } from '../../testing/state'; + +import { createRefineSchema } from './refine'; + +extendZodWithOpenApi(z); + +describe('createRefineSchema', () => { + it('returns a schema when creating an output schema with preprocess', () => { + const expected: oas31.SchemaObject = { + type: 'string', + }; + const schema = z.string().refine((check) => typeof check === 'string'); + + const result = createRefineSchema(schema, createOutputState()); + + expect(result).toStrictEqual(expected); + }); +}); diff --git a/src/create/schema/refine.ts b/src/create/schema/refine.ts new file mode 100644 index 0000000..96876e7 --- /dev/null +++ b/src/create/schema/refine.ts @@ -0,0 +1,10 @@ +import { oas31 } from 'openapi3-ts'; +import { ZodEffects, ZodType } from 'zod'; + +import { SchemaState, createSchemaOrRef } from '.'; + +export const createRefineSchema = ( + zodRefine: ZodEffects, + state: SchemaState, +): oas31.SchemaObject | oas31.ReferenceObject => + createSchemaOrRef(zodRefine._def.schema as ZodType, state); diff --git a/src/create/schema/transform.test.ts b/src/create/schema/transform.test.ts new file mode 100644 index 0000000..ddab947 --- /dev/null +++ b/src/create/schema/transform.test.ts @@ -0,0 +1,57 @@ +import { oas31 } from 'openapi3-ts'; +import { z } from 'zod'; + +import { extendZodWithOpenApi } from '../../extendZod'; +import { createInputState, createOutputState } from '../../testing/state'; + +import { createTransformSchema } from './transform'; + +extendZodWithOpenApi(z); + +describe('createTransformSchema', () => { + describe('input', () => { + it('creates a schema from transform', () => { + const expected: oas31.SchemaObject = { + type: 'string', + }; + const schema = z.string().transform((str) => str.length); + + const result = createTransformSchema(schema, createInputState()); + + expect(result).toStrictEqual(expected); + }); + + it('changes the state effectType to input', () => { + const schema = z.string().transform((str) => str.length); + const state = createInputState(); + + createTransformSchema(schema, state); + + expect(state.effectType).toBe('input'); + }); + }); + + describe('output', () => { + it('throws an error with a schema with transform', () => { + const schema = z.string().transform((str) => str.length); + + expect(() => + createTransformSchema(schema, createOutputState()), + ).toThrow(); + }); + + it('creates an empty schema when a type is manually specified', () => { + const expected: oas31.SchemaObject = { + type: 'number', + }; + const schema = z + .string() + .transform((str) => str.length) + .openapi({ type: 'number' }); + + const result = createTransformSchema(schema, createOutputState()); + + expect(result).toStrictEqual(expected); + }); + }); +}); diff --git a/src/create/schema/transform.ts b/src/create/schema/transform.ts new file mode 100644 index 0000000..7911c23 --- /dev/null +++ b/src/create/schema/transform.ts @@ -0,0 +1,18 @@ +import { oas31 } from 'openapi3-ts'; +import { ZodEffects, ZodType } from 'zod'; + +import { createUnknownSchema } from './unknown'; + +import { SchemaState, createSchemaOrRef } from '.'; + +export const createTransformSchema = ( + zodTransform: ZodEffects, + state: SchemaState, +): oas31.SchemaObject | oas31.ReferenceObject => { + if (state.type === 'input') { + state.effectType = 'input'; + return createSchemaOrRef(zodTransform._def.schema as ZodType, state); + } + + return createUnknownSchema(zodTransform); +}; diff --git a/src/create/schema/tuple.test.ts b/src/create/schema/tuple.test.ts index 47dc50e..45e454a 100644 --- a/src/create/schema/tuple.test.ts +++ b/src/create/schema/tuple.test.ts @@ -2,7 +2,10 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { + createOutputOpenapi3State, + createOutputState, +} from '../../testing/state'; import { createTupleSchema } from './tuple'; @@ -25,7 +28,7 @@ describe('createTupleSchema', () => { }; const schema = z.tuple([z.string(), z.number()]); - const result = createTupleSchema(schema, getDefaultComponents()); + const result = createTupleSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -47,7 +50,7 @@ describe('createTupleSchema', () => { }; const schema = z.tuple([z.string(), z.number()]).rest(z.boolean()); - const result = createTupleSchema(schema, getDefaultComponents()); + const result = createTupleSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -60,7 +63,7 @@ describe('createTupleSchema', () => { }; const schema = z.tuple([]); - const result = createTupleSchema(schema, getDefaultComponents()); + const result = createTupleSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); @@ -84,7 +87,7 @@ describe('createTupleSchema', () => { }; const schema = z.tuple([z.string(), z.number()]).rest(z.boolean()); - const result = createTupleSchema(schema, getDefaultComponents({}, '3.0.0')); + const result = createTupleSchema(schema, createOutputOpenapi3State()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/tuple.ts b/src/create/schema/tuple.ts index d24fab8..2569a00 100644 --- a/src/create/schema/tuple.ts +++ b/src/create/schema/tuple.ts @@ -2,28 +2,27 @@ import { oas31 } from 'openapi3-ts'; import { ZodTuple, ZodTypeAny } from 'zod'; import { satisfiesVersion } from '../../openapi'; -import { ComponentsObject } from '../components'; -import { createSchemaOrRef } from '.'; +import { SchemaState, createSchemaOrRef } from '.'; export const createTupleSchema = ( zodTuple: ZodTuple, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject => { const items = zodTuple.items as ZodTypeAny[]; const rest = zodTuple._def.rest as ZodTypeAny; return { type: 'array', - ...mapItemProperties(items, rest, components), + ...mapItemProperties(items, rest, state), } as oas31.SchemaObject; }; const mapPrefixItems = ( items: ZodTypeAny[], - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject['prefixItems'] | undefined => { if (items.length) { - return items.map((item) => createSchemaOrRef(item, components)); + return items.map((item) => createSchemaOrRef(item, state)); } return undefined; }; @@ -31,14 +30,14 @@ const mapPrefixItems = ( const mapItemProperties = ( items: ZodTypeAny[], rest: ZodTypeAny, - components: ComponentsObject, + state: SchemaState, ): Pick< oas31.SchemaObject, 'items' | 'minItems' | 'maxItems' | 'prefixItems' > => { - const prefixItems = mapPrefixItems(items, components); + const prefixItems = mapPrefixItems(items, state); - if (satisfiesVersion(components.openapi, '3.1.0')) { + if (satisfiesVersion(state.components.openapi, '3.1.0')) { if (!rest) { return { maxItems: items.length, @@ -48,7 +47,7 @@ const mapItemProperties = ( } return { - items: createSchemaOrRef(rest, components), + items: createSchemaOrRef(rest, state), ...(prefixItems && { prefixItems }), }; } @@ -63,7 +62,9 @@ const mapItemProperties = ( return { ...(prefixItems && { - items: { oneOf: [...prefixItems, createSchemaOrRef(rest, components)] }, + items: { + oneOf: [...prefixItems, createSchemaOrRef(rest, state)], + }, }), }; }; diff --git a/src/create/schema/union.test.ts b/src/create/schema/union.test.ts index 65a2af3..ab6df7a 100644 --- a/src/create/schema/union.test.ts +++ b/src/create/schema/union.test.ts @@ -2,7 +2,7 @@ import { oas31 } from 'openapi3-ts'; import { z } from 'zod'; import { extendZodWithOpenApi } from '../../extendZod'; -import { getDefaultComponents } from '../components'; +import { createOutputState } from '../../testing/state'; import { createUnionSchema } from './union'; @@ -22,7 +22,7 @@ describe('createUnionSchema', () => { }; const schema = z.union([z.string(), z.number()]); - const result = createUnionSchema(schema, getDefaultComponents()); + const result = createUnionSchema(schema, createOutputState()); expect(result).toStrictEqual(expected); }); diff --git a/src/create/schema/union.ts b/src/create/schema/union.ts index 6fbce7d..0c8e613 100644 --- a/src/create/schema/union.ts +++ b/src/create/schema/union.ts @@ -1,18 +1,14 @@ import { oas31 } from 'openapi3-ts'; import { ZodTypeAny, ZodUnion } from 'zod'; -import { ComponentsObject } from '../components'; - -import { createSchemaOrRef } from '.'; +import { SchemaState, createSchemaOrRef } from '.'; export const createUnionSchema = ( zodUnion: ZodUnion, - components: ComponentsObject, + state: SchemaState, ): oas31.SchemaObject => { const options = zodUnion.options as ZodTypeAny[]; - const schemas = options.map((option) => - createSchemaOrRef(option, components), - ); + const schemas = options.map((option) => createSchemaOrRef(option, state)); return { anyOf: schemas, }; diff --git a/src/create/schema/unknown.test.ts b/src/create/schema/unknown.test.ts new file mode 100644 index 0000000..d0fd70f --- /dev/null +++ b/src/create/schema/unknown.test.ts @@ -0,0 +1,21 @@ +import { oas31 } from 'openapi3-ts'; +import { z } from 'zod'; + +import { extendZodWithOpenApi } from '../../extendZod'; + +import { createUnknownSchema } from './unknown'; + +extendZodWithOpenApi(z); + +describe('createUnknownSchema', () => { + it('creates a simple string schema for an optional string', () => { + const expected: oas31.SchemaObject = { + type: 'string', + }; + const schema = z.unknown().openapi({ type: 'string' }); + + const result = createUnknownSchema(schema); + + expect(result).toStrictEqual(expected); + }); +}); diff --git a/src/create/schema/unknown.ts b/src/create/schema/unknown.ts new file mode 100644 index 0000000..880c4dc --- /dev/null +++ b/src/create/schema/unknown.ts @@ -0,0 +1,25 @@ +import { oas31 } from 'openapi3-ts'; +import { ZodEffects, ZodType, ZodTypeDef } from 'zod'; + +export const createUnknownSchema = < + Output = any, + Def extends ZodTypeDef = ZodTypeDef, + Input = Output, +>( + zodSchema: ZodType, +): oas31.SchemaObject => { + if (!zodSchema._def.openapi?.type) { + const zodType = zodSchema.constructor.name; + const schemaName = + zodSchema instanceof ZodEffects + ? `${zodType} - ${zodSchema._def.effect.type}` + : zodType; + throw new Error( + `Unknown schema ${schemaName}. Please assign it a manual type`, + ); + } + + return { + type: zodSchema._def.openapi.type, + }; +}; diff --git a/src/extendZod.ts b/src/extendZod.ts index 7a20ba4..173a529 100644 --- a/src/extendZod.ts +++ b/src/extendZod.ts @@ -8,6 +8,8 @@ import { z, } from 'zod'; +import { CreationType } from './create/components'; + type SchemaObject = oas30.SchemaObject & oas31.SchemaObject; interface ZodOpenApiMetadata> @@ -15,7 +17,14 @@ interface ZodOpenApiMetadata> example?: TInferred; examples?: [TInferred, ...TInferred[]]; default?: T extends ZodDate ? string : TInferred; + /** + * Use this field to output this Zod Schema in the components schemas section. Any usage of this Zod Schema will then be transformed into a $ref. + */ ref?: string; + /** + * Use this field when you are manually adding a Zod Schema to the components section. This controls whether this should be rendered as request (`input`) or response (`output`). Defaults to `output` + */ + refType?: CreationType; param?: Partial & { example?: TInferred; examples?: { @@ -23,9 +32,15 @@ interface ZodOpenApiMetadata> | (oas31.ExampleObject & { value: TInferred }) | oas31.ReferenceObject; }; + /** + * Use this field to output this Zod Schema in the components parameters section. Any usage of this Zod Schema will then be transformed into a $ref. + */ ref?: string; }; header?: Partial & { + /** + * Use this field to output this Zod Schema in the components headers section. Any usage of this Zod Schema will then be transformed into a $ref. + */ ref?: string; }; } diff --git a/src/testing/state.ts b/src/testing/state.ts new file mode 100644 index 0000000..39476af --- /dev/null +++ b/src/testing/state.ts @@ -0,0 +1,17 @@ +import { getDefaultComponents } from '../create/components'; +import { SchemaState } from '../create/schema'; + +export const createOutputState = (): SchemaState => ({ + components: getDefaultComponents(), + type: 'output', +}); + +export const createInputState = (): SchemaState => ({ + components: getDefaultComponents(), + type: 'input', +}); + +export const createOutputOpenapi3State = (): SchemaState => ({ + components: { ...getDefaultComponents(), openapi: '3.0.0' }, + type: 'output', +});