From 456456f6510ecd88f32bfbd24c0a83b1181b7946 Mon Sep 17 00:00:00 2001 From: Marko Calasan Date: Thu, 26 Oct 2023 02:18:01 +0200 Subject: [PATCH 1/5] Zod-to-openapi --- src/generator/schema.ts | 32 ++++++++++++++++++++++++++++---- src/types.ts | 21 ++++++++++++++++++++- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/generator/schema.ts b/src/generator/schema.ts index d3f41e38..2c067cc9 100644 --- a/src/generator/schema.ts +++ b/src/generator/schema.ts @@ -3,7 +3,7 @@ import { OpenAPIV3 } from 'openapi-types'; import { z } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; -import { OpenApiContentType } from '../types'; +import { OpenApiContentType, ZodToOpenApiRegistry } from '../types'; import { instanceofZodType, instanceofZodTypeCoercible, @@ -15,9 +15,33 @@ import { zodSupportsCoerce, } from '../utils/zod'; +let zodComponentDefinitions: Record = {}; + const zodSchemaToOpenApiSchemaObject = (zodSchema: z.ZodType): OpenAPIV3.SchemaObject => { // FIXME: https://github.com/StefanTerdell/zod-to-json-schema/issues/35 - return zodToJsonSchema(zodSchema, { target: 'openApi3', $refStrategy: 'none' }) as any; + const result = zodToJsonSchema(zodSchema, { + target: 'openApi3', + definitions: zodComponentDefinitions, + definitionPath: 'components/schemas', + }) as any; + delete result['components/schemas']; + return result; +}; + +export const setZodComponentDefinitions = (definitions: Record) => { + zodComponentDefinitions = definitions; +}; + +export const setZodComponentRegistry = (registry: ZodToOpenApiRegistry) => { + const mapped = registry.definitions.reduce((acc, d) => { + const refId = d.schema?._def.openapi?._internal?.refId; + if (d.type === 'schema' && refId && d.schema) { + acc[refId] = d.schema; + } + return acc; + }, {} as { [key: string]: z.ZodType }); + + setZodComponentDefinitions(mapped); }; export const getParameterObjects = ( @@ -156,7 +180,7 @@ export const getRequestBodyObject = ( return undefined; } - const openApiSchemaObject = zodSchemaToOpenApiSchemaObject(dedupedSchema); + const openApiSchemaObject = zodSchemaToOpenApiSchemaObject(unwrappedSchema); const content: OpenAPIV3.RequestBodyObject['content'] = {}; for (const contentType of contentTypes) { content[contentType] = { @@ -189,7 +213,7 @@ export const errorResponseObject: OpenAPIV3.ResponseObject = { export const getResponsesObject = ( schema: unknown, example: Record | undefined, - headers: Record | undefined + headers: Record | undefined, ): OpenAPIV3.ResponsesObject => { if (!instanceofZodType(schema)) { throw new TRPCError({ diff --git a/src/types.ts b/src/types.ts index 71cec3ee..8b2e25cd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ import type { RootConfig } from '@trpc/server/dist/core/internals/config'; import { TRPC_ERROR_CODE_KEY } from '@trpc/server/rpc'; import type { RouterDef } from '@trpc/server/src/core/router'; import { OpenAPIV3 } from 'openapi-types'; -import { ZodIssue } from 'zod'; +import { ZodIssue, z } from 'zod'; export type OpenApiMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; @@ -77,3 +77,22 @@ export type OpenApiErrorResponse = { }; export type OpenApiResponse = OpenApiSuccessResponse | OpenApiErrorResponse; + +export type ZodToOpenApiRegistry = { + definitions: ZodToOpenApiRegistryDefinition[]; +}; + +export type ZodToOpenApiRegistryDefinition = { + type: string; + schema?: z.ZodType< + any, + z.ZodTypeDef & { + openapi?: { + _internal?: { + refId?: string; + }; + }; + }, + any + >; +}; From 04c0bf8290c774a60c4818efd06708df5759fc8b Mon Sep 17 00:00:00 2001 From: Marko Calasan Date: Thu, 26 Oct 2023 03:58:41 +0200 Subject: [PATCH 2/5] Zod-to-openapi generator, registry, exports --- src/generator/index.ts | 12 ++++++++++ src/generator/schema.ts | 42 ++++++++++++++++------------------- src/index.ts | 8 +++++++ src/types.ts | 24 +++++++++++--------- src/utils/registry.ts | 49 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 34 deletions(-) create mode 100644 src/utils/registry.ts diff --git a/src/generator/index.ts b/src/generator/index.ts index 3f54871c..45bb0970 100644 --- a/src/generator/index.ts +++ b/src/generator/index.ts @@ -1,6 +1,11 @@ import { OpenAPIV3 } from 'openapi-types'; import { OpenApiRouter } from '../types'; +import { + builtInZodComponentGenerator, + zodComponentGenerator, + zodComponentRegistry, +} from '../utils/registry'; import { getOpenApiPathsObject } from './paths'; import { errorResponseObject } from './schema'; @@ -41,6 +46,13 @@ export const generateOpenApiDocument = ( paths: getOpenApiPathsObject(appRouter, Object.keys(securitySchemes)), components: { securitySchemes, + schemas: { + ...(zodComponentGenerator === 'built-in' + ? builtInZodComponentGenerator() + : zodComponentGenerator !== 'none' && zodComponentRegistry + ? zodComponentGenerator(zodComponentRegistry) + : {}), + }, responses: { error: errorResponseObject, }, diff --git a/src/generator/schema.ts b/src/generator/schema.ts index 2c067cc9..786ac72a 100644 --- a/src/generator/schema.ts +++ b/src/generator/schema.ts @@ -3,7 +3,8 @@ import { OpenAPIV3 } from 'openapi-types'; import { z } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; -import { OpenApiContentType, ZodToOpenApiRegistry } from '../types'; +import { OpenApiContentType, ZodToOpenApiSchema } from '../types'; +import { zodComponentDefinitions } from '../utils/registry'; import { instanceofZodType, instanceofZodTypeCoercible, @@ -15,33 +16,28 @@ import { zodSupportsCoerce, } from '../utils/zod'; -let zodComponentDefinitions: Record = {}; - -const zodSchemaToOpenApiSchemaObject = (zodSchema: z.ZodType): OpenAPIV3.SchemaObject => { +export const zodSchemaToOpenApiSchemaObject = ( + zodSchema: z.ZodType, + skipCurrentObjectRef = false, +): OpenAPIV3.SchemaObject => { // FIXME: https://github.com/StefanTerdell/zod-to-json-schema/issues/35 + const casted = zodSchema as ZodToOpenApiSchema; + const refId = casted._def.openapi?._internal?.refId; + + let processedDefinitions = zodComponentDefinitions; + if (refId && skipCurrentObjectRef && zodComponentDefinitions) { + const { [refId]: _, ...definitionsWithoutCurr } = zodComponentDefinitions; + processedDefinitions = definitionsWithoutCurr; + } + const result = zodToJsonSchema(zodSchema, { target: 'openApi3', - definitions: zodComponentDefinitions, + definitions: processedDefinitions || {}, definitionPath: 'components/schemas', - }) as any; - delete result['components/schemas']; - return result; -}; - -export const setZodComponentDefinitions = (definitions: Record) => { - zodComponentDefinitions = definitions; -}; - -export const setZodComponentRegistry = (registry: ZodToOpenApiRegistry) => { - const mapped = registry.definitions.reduce((acc, d) => { - const refId = d.schema?._def.openapi?._internal?.refId; - if (d.type === 'schema' && refId && d.schema) { - acc[refId] = d.schema; - } - return acc; - }, {} as { [key: string]: z.ZodType }); + }); - setZodComponentDefinitions(mapped); + delete (result as any)['components/schemas']; + return result as any; }; export const getParameterObjects = ( diff --git a/src/index.ts b/src/index.ts index 4d89b3ff..7fa420e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,11 @@ import { OpenApiRouter, OpenApiSuccessResponse, } from './types'; +import { + setZodComponentDefinitions, + setZodComponentGenerator, + setZodComponentRegistry, +} from './utils/registry'; import { ZodTypeLikeString, ZodTypeLikeVoid } from './utils/zod'; export { @@ -51,4 +56,7 @@ export { OpenApiErrorResponse, ZodTypeLikeString, ZodTypeLikeVoid, + setZodComponentDefinitions, + setZodComponentRegistry, + setZodComponentGenerator, }; diff --git a/src/types.ts b/src/types.ts index 8b2e25cd..1cf5169d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -84,15 +84,17 @@ export type ZodToOpenApiRegistry = { export type ZodToOpenApiRegistryDefinition = { type: string; - schema?: z.ZodType< - any, - z.ZodTypeDef & { - openapi?: { - _internal?: { - refId?: string; - }; - }; - }, - any - >; + schema?: ZodToOpenApiSchema; }; + +export type ZodToOpenApiSchema = z.ZodType< + any, + z.ZodTypeDef & { + openapi?: { + _internal?: { + refId?: string; + }; + }; + }, + any +>; diff --git a/src/utils/registry.ts b/src/utils/registry.ts new file mode 100644 index 00000000..f58ca645 --- /dev/null +++ b/src/utils/registry.ts @@ -0,0 +1,49 @@ +import { OpenAPIV3 } from 'openapi-types'; +import { z } from 'zod'; + +import { zodSchemaToOpenApiSchemaObject } from '../generator/schema'; +import { ZodToOpenApiRegistry } from '../types'; + +export let zodComponentRegistry: ZodToOpenApiRegistry | undefined; + +export let zodComponentDefinitions: Record | undefined; + +export let zodComponentGenerator: + | ((registry: ZodToOpenApiRegistry) => { + [key: string]: any; + }) + | 'built-in' + | 'none' = 'none'; + +export const setZodComponentDefinitions = (definitions: Record) => { + zodComponentDefinitions = definitions; +}; + +export const setZodComponentRegistry = (registry: ZodToOpenApiRegistry) => { + zodComponentRegistry = registry; + + const mapped = registry.definitions.reduce((acc, d) => { + const refId = d.schema?._def.openapi?._internal?.refId; + if (d.type === 'schema' && refId && d.schema) { + acc[refId] = d.schema; + } + return acc; + }, {} as { [key: string]: z.ZodType }); + + setZodComponentDefinitions(mapped); +}; + +export const setZodComponentGenerator = (generator: typeof zodComponentGenerator) => { + zodComponentGenerator = generator; +}; + +export const builtInZodComponentGenerator = (): { [key: string]: OpenAPIV3.SchemaObject } => { + return zodComponentDefinitions + ? Object.fromEntries( + Object.entries(zodComponentDefinitions).map(([key, value]) => [ + key, + zodSchemaToOpenApiSchemaObject(value, true), + ]), + ) + : {}; +}; From ce1210751c9e95a0b043c4a3882bbfa34be7838f Mon Sep 17 00:00:00 2001 From: Marko Calasan Date: Thu, 26 Oct 2023 13:37:11 +0200 Subject: [PATCH 3/5] Cleanup --- src/generator/index.ts | 14 ++---------- src/generator/schema.ts | 26 +++++++++------------- src/index.ts | 10 ++++----- src/types.ts | 21 ------------------ src/utils/components.ts | 33 +++++++++++++++++++++++++++ src/utils/registry.ts | 49 ----------------------------------------- 6 files changed, 50 insertions(+), 103 deletions(-) create mode 100644 src/utils/components.ts delete mode 100644 src/utils/registry.ts diff --git a/src/generator/index.ts b/src/generator/index.ts index 45bb0970..66d5b484 100644 --- a/src/generator/index.ts +++ b/src/generator/index.ts @@ -1,11 +1,7 @@ import { OpenAPIV3 } from 'openapi-types'; import { OpenApiRouter } from '../types'; -import { - builtInZodComponentGenerator, - zodComponentGenerator, - zodComponentRegistry, -} from '../utils/registry'; +import { zodComponentProcessor } from '../utils/components'; import { getOpenApiPathsObject } from './paths'; import { errorResponseObject } from './schema'; @@ -46,13 +42,7 @@ export const generateOpenApiDocument = ( paths: getOpenApiPathsObject(appRouter, Object.keys(securitySchemes)), components: { securitySchemes, - schemas: { - ...(zodComponentGenerator === 'built-in' - ? builtInZodComponentGenerator() - : zodComponentGenerator !== 'none' && zodComponentRegistry - ? zodComponentGenerator(zodComponentRegistry) - : {}), - }, + schemas: zodComponentProcessor?.generateSchemas?.(), responses: { error: errorResponseObject, }, diff --git a/src/generator/schema.ts b/src/generator/schema.ts index 786ac72a..018ba29b 100644 --- a/src/generator/schema.ts +++ b/src/generator/schema.ts @@ -3,8 +3,8 @@ import { OpenAPIV3 } from 'openapi-types'; import { z } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; -import { OpenApiContentType, ZodToOpenApiSchema } from '../types'; -import { zodComponentDefinitions } from '../utils/registry'; +import { OpenApiContentType } from '../types'; +import { zodComponentDefinitions } from '../utils/components'; import { instanceofZodType, instanceofZodTypeCoercible, @@ -18,26 +18,20 @@ import { export const zodSchemaToOpenApiSchemaObject = ( zodSchema: z.ZodType, - skipCurrentObjectRef = false, + suppressObjectReferences = false, ): OpenAPIV3.SchemaObject => { // FIXME: https://github.com/StefanTerdell/zod-to-json-schema/issues/35 - const casted = zodSchema as ZodToOpenApiSchema; - const refId = casted._def.openapi?._internal?.refId; - - let processedDefinitions = zodComponentDefinitions; - if (refId && skipCurrentObjectRef && zodComponentDefinitions) { - const { [refId]: _, ...definitionsWithoutCurr } = zodComponentDefinitions; - processedDefinitions = definitionsWithoutCurr; - } - const result = zodToJsonSchema(zodSchema, { target: 'openApi3', - definitions: processedDefinitions || {}, + definitions: + zodComponentDefinitions && !suppressObjectReferences ? zodComponentDefinitions : {}, definitionPath: 'components/schemas', - }); + }) as OpenAPIV3.SchemaObject & { + 'components/schemas': unknown; + }; - delete (result as any)['components/schemas']; - return result as any; + delete result['components/schemas']; + return result; }; export const getParameterObjects = ( diff --git a/src/index.ts b/src/index.ts index 7fa420e6..7180f340 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,10 +26,10 @@ import { OpenApiSuccessResponse, } from './types'; import { + experimentalZodSchemaGenerator, setZodComponentDefinitions, - setZodComponentGenerator, - setZodComponentRegistry, -} from './utils/registry'; + setZodComponentProcessor, +} from './utils/components'; import { ZodTypeLikeString, ZodTypeLikeVoid } from './utils/zod'; export { @@ -57,6 +57,6 @@ export { ZodTypeLikeString, ZodTypeLikeVoid, setZodComponentDefinitions, - setZodComponentRegistry, - setZodComponentGenerator, + setZodComponentProcessor, + experimentalZodSchemaGenerator, }; diff --git a/src/types.ts b/src/types.ts index 1cf5169d..0699a44e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -77,24 +77,3 @@ export type OpenApiErrorResponse = { }; export type OpenApiResponse = OpenApiSuccessResponse | OpenApiErrorResponse; - -export type ZodToOpenApiRegistry = { - definitions: ZodToOpenApiRegistryDefinition[]; -}; - -export type ZodToOpenApiRegistryDefinition = { - type: string; - schema?: ZodToOpenApiSchema; -}; - -export type ZodToOpenApiSchema = z.ZodType< - any, - z.ZodTypeDef & { - openapi?: { - _internal?: { - refId?: string; - }; - }; - }, - any ->; diff --git a/src/utils/components.ts b/src/utils/components.ts new file mode 100644 index 00000000..b4866f61 --- /dev/null +++ b/src/utils/components.ts @@ -0,0 +1,33 @@ +import { OpenAPIV3 } from 'openapi-types'; +import { z } from 'zod'; + +import { zodSchemaToOpenApiSchemaObject } from '../generator/schema'; + +type ZodComponentProcessor = { + getComponentRefId: (schema: z.ZodType) => string | undefined; + generateSchemas?: () => { [key: string]: any }; +}; + +export let zodComponentProcessor: ZodComponentProcessor | undefined = undefined; + +export let zodComponentDefinitions: Record | undefined; + +export const setZodComponentDefinitions = (definitions: Record) => { + zodComponentDefinitions = definitions; +}; + +export const setZodComponentProcessor = (processor: ZodComponentProcessor) => { + zodComponentProcessor = processor; +}; + +// Does not support references (breaks in weird ways if references are used) +export const experimentalZodSchemaGenerator = (): { [key: string]: OpenAPIV3.SchemaObject } => { + return zodComponentDefinitions + ? Object.fromEntries( + Object.entries(zodComponentDefinitions).map(([key, value]) => [ + key, + zodSchemaToOpenApiSchemaObject(value, true), + ]), + ) + : {}; +}; diff --git a/src/utils/registry.ts b/src/utils/registry.ts deleted file mode 100644 index f58ca645..00000000 --- a/src/utils/registry.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { OpenAPIV3 } from 'openapi-types'; -import { z } from 'zod'; - -import { zodSchemaToOpenApiSchemaObject } from '../generator/schema'; -import { ZodToOpenApiRegistry } from '../types'; - -export let zodComponentRegistry: ZodToOpenApiRegistry | undefined; - -export let zodComponentDefinitions: Record | undefined; - -export let zodComponentGenerator: - | ((registry: ZodToOpenApiRegistry) => { - [key: string]: any; - }) - | 'built-in' - | 'none' = 'none'; - -export const setZodComponentDefinitions = (definitions: Record) => { - zodComponentDefinitions = definitions; -}; - -export const setZodComponentRegistry = (registry: ZodToOpenApiRegistry) => { - zodComponentRegistry = registry; - - const mapped = registry.definitions.reduce((acc, d) => { - const refId = d.schema?._def.openapi?._internal?.refId; - if (d.type === 'schema' && refId && d.schema) { - acc[refId] = d.schema; - } - return acc; - }, {} as { [key: string]: z.ZodType }); - - setZodComponentDefinitions(mapped); -}; - -export const setZodComponentGenerator = (generator: typeof zodComponentGenerator) => { - zodComponentGenerator = generator; -}; - -export const builtInZodComponentGenerator = (): { [key: string]: OpenAPIV3.SchemaObject } => { - return zodComponentDefinitions - ? Object.fromEntries( - Object.entries(zodComponentDefinitions).map(([key, value]) => [ - key, - zodSchemaToOpenApiSchemaObject(value, true), - ]), - ) - : {}; -}; From 90613744414f88468ce20366081f9462901f50cc Mon Sep 17 00:00:00 2001 From: Marko Calasan Date: Thu, 26 Oct 2023 13:37:48 +0200 Subject: [PATCH 4/5] Removed unused type --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 0699a44e..71cec3ee 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ import type { RootConfig } from '@trpc/server/dist/core/internals/config'; import { TRPC_ERROR_CODE_KEY } from '@trpc/server/rpc'; import type { RouterDef } from '@trpc/server/src/core/router'; import { OpenAPIV3 } from 'openapi-types'; -import { ZodIssue, z } from 'zod'; +import { ZodIssue } from 'zod'; export type OpenApiMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; From 13d30e830f647c8c093f64a90064b021332fa28f Mon Sep 17 00:00:00 2001 From: Marko Calasan Date: Thu, 26 Oct 2023 13:56:52 +0200 Subject: [PATCH 5/5] Processor -> Schema generator --- src/generator/index.ts | 4 ++-- src/index.ts | 4 ++-- src/utils/components.ts | 11 +++-------- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/generator/index.ts b/src/generator/index.ts index 66d5b484..a21a930f 100644 --- a/src/generator/index.ts +++ b/src/generator/index.ts @@ -1,7 +1,7 @@ import { OpenAPIV3 } from 'openapi-types'; import { OpenApiRouter } from '../types'; -import { zodComponentProcessor } from '../utils/components'; +import { zodComponentSchemaGenerator } from '../utils/components'; import { getOpenApiPathsObject } from './paths'; import { errorResponseObject } from './schema'; @@ -42,7 +42,7 @@ export const generateOpenApiDocument = ( paths: getOpenApiPathsObject(appRouter, Object.keys(securitySchemes)), components: { securitySchemes, - schemas: zodComponentProcessor?.generateSchemas?.(), + schemas: zodComponentSchemaGenerator?.(), responses: { error: errorResponseObject, }, diff --git a/src/index.ts b/src/index.ts index 7180f340..8b3127f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,7 @@ import { import { experimentalZodSchemaGenerator, setZodComponentDefinitions, - setZodComponentProcessor, + setZodComponentSchemaGenerator, } from './utils/components'; import { ZodTypeLikeString, ZodTypeLikeVoid } from './utils/zod'; @@ -57,6 +57,6 @@ export { ZodTypeLikeString, ZodTypeLikeVoid, setZodComponentDefinitions, - setZodComponentProcessor, + setZodComponentSchemaGenerator, experimentalZodSchemaGenerator, }; diff --git a/src/utils/components.ts b/src/utils/components.ts index b4866f61..c3ddbe0b 100644 --- a/src/utils/components.ts +++ b/src/utils/components.ts @@ -3,12 +3,7 @@ import { z } from 'zod'; import { zodSchemaToOpenApiSchemaObject } from '../generator/schema'; -type ZodComponentProcessor = { - getComponentRefId: (schema: z.ZodType) => string | undefined; - generateSchemas?: () => { [key: string]: any }; -}; - -export let zodComponentProcessor: ZodComponentProcessor | undefined = undefined; +export let zodComponentSchemaGenerator: (() => { [key: string]: any }) | undefined; export let zodComponentDefinitions: Record | undefined; @@ -16,8 +11,8 @@ export const setZodComponentDefinitions = (definitions: Record { - zodComponentProcessor = processor; +export const setZodComponentSchemaGenerator = (generator: typeof zodComponentSchemaGenerator) => { + zodComponentSchemaGenerator = generator; }; // Does not support references (breaks in weird ways if references are used)