@@ -13,6 +13,7 @@ import {
1313 OpenAPIObject ,
1414 OperationObject ,
1515 PathsObject ,
16+ ReferenceObject ,
1617 SchemaObject ,
1718} from 'openapi3-ts' ;
1819import { generateSchema } from '@anatine/zod-openapi' ;
@@ -187,6 +188,85 @@ const convertSchemaObjectToMediaTypeObject = (
187188 } ;
188189} ;
189190
191+ const extractReferenceSchemas = (
192+ schema : SchemaObject ,
193+ referenceSchemas : { [ id : string ] : SchemaObject } ,
194+ ) : SchemaObject => {
195+ if ( schema . allOf ) {
196+ schema . allOf = schema . allOf ?. map ( ( subSchema ) =>
197+ extractReferenceSchemas ( subSchema , referenceSchemas ) ,
198+ ) ;
199+ }
200+
201+ if ( schema . anyOf ) {
202+ schema . anyOf = schema . anyOf ?. map ( ( subSchema ) =>
203+ extractReferenceSchemas ( subSchema , referenceSchemas ) ,
204+ ) ;
205+ }
206+
207+ if ( schema . oneOf ) {
208+ schema . oneOf = schema . oneOf ?. map ( ( subSchema ) =>
209+ extractReferenceSchemas ( subSchema , referenceSchemas ) ,
210+ ) ;
211+ }
212+
213+ if ( schema . not ) {
214+ schema . not = extractReferenceSchemas ( schema . not , referenceSchemas ) ;
215+ }
216+
217+ if ( schema . items ) {
218+ schema . items = extractReferenceSchemas ( schema . items , referenceSchemas ) ;
219+ }
220+
221+ if ( schema . properties ) {
222+ schema . properties = Object . entries ( schema . properties ) . reduce < {
223+ [ p : string ] : SchemaObject | ReferenceObject ;
224+ } > ( ( prev , [ propertyName , schema ] ) => {
225+ prev [ propertyName ] = extractReferenceSchemas ( schema , referenceSchemas ) ;
226+ return prev ;
227+ } , { } ) ;
228+ }
229+
230+ if ( schema . additionalProperties ) {
231+ schema . additionalProperties =
232+ typeof schema . additionalProperties != 'boolean'
233+ ? extractReferenceSchemas ( schema . additionalProperties , referenceSchemas )
234+ : schema . additionalProperties ;
235+ }
236+
237+ if ( schema . title ) {
238+ const nullable = schema . nullable ;
239+ schema . nullable = undefined ;
240+ if ( schema . title in referenceSchemas ) {
241+ if (
242+ JSON . stringify ( referenceSchemas [ schema . title ] ) !==
243+ JSON . stringify ( schema )
244+ ) {
245+ throw new Error (
246+ `Schema title '${ schema . title } ' already exists with a different schema` ,
247+ ) ;
248+ }
249+ } else {
250+ referenceSchemas [ schema . title ] = schema ;
251+ }
252+
253+ if ( nullable ) {
254+ schema = {
255+ nullable : true ,
256+ allOf : [
257+ {
258+ $ref : `#/components/schemas/${ schema . title } ` ,
259+ } ,
260+ ] ,
261+ } ;
262+ } else {
263+ schema = {
264+ $ref : `#/components/schemas/${ schema . title } ` ,
265+ } ;
266+ }
267+ }
268+ return schema ;
269+ } ;
190270/**
191271 *
192272 * @param options.jsonQuery - Enable JSON query parameters, [see](/docs/open-api#json-query-params)
@@ -216,6 +296,8 @@ export const generateOpenApi = (
216296
217297 const operationIds = new Map < string , string [ ] > ( ) ;
218298
299+ const referenceSchemas : { [ id : string ] : SchemaObject } = { } ;
300+
219301 const pathObject = paths . reduce ( ( acc , path ) => {
220302 if ( options . setOperationId === true ) {
221303 const existingOp = operationIds . get ( path . id ) ;
@@ -235,19 +317,30 @@ export const generateOpenApi = (
235317 ! ! options . jsonQuery ,
236318 ) ;
237319
238- const bodySchema =
320+ let bodySchema =
239321 path . route ?. method !== 'GET' && 'body' in path . route
240322 ? getOpenApiSchemaFromZod ( path . route . body )
241323 : null ;
242324
325+ if ( bodySchema ?. title ) {
326+ bodySchema = extractReferenceSchemas ( bodySchema , referenceSchemas ) ;
327+ }
328+
243329 const responses = Object . entries ( path . route . responses ) . reduce (
244330 ( acc , [ statusCode , response ] ) => {
245- const responseSchema = getOpenApiSchemaFromZod ( response , true ) ;
331+ let responseSchema = getOpenApiSchemaFromZod ( response , true ) ;
246332 const description =
247333 isZodType ( response ) && response . description
248334 ? response . description
249335 : statusCode ;
250336
337+ if ( responseSchema ) {
338+ responseSchema = extractReferenceSchemas (
339+ responseSchema ,
340+ referenceSchemas ,
341+ ) ;
342+ }
343+
251344 return {
252345 ...acc ,
253346 [ statusCode ] : {
@@ -311,6 +404,15 @@ export const generateOpenApi = (
311404 return acc ;
312405 } , { } as PathsObject ) ;
313406
407+ if ( Object . keys ( referenceSchemas ) . length ) {
408+ apiDoc [ 'components' ] = {
409+ schemas : {
410+ ...referenceSchemas ,
411+ } ,
412+ ...apiDoc [ 'components' ] ,
413+ } ;
414+ }
415+
314416 return {
315417 openapi : '3.0.2' ,
316418 paths : pathObject ,
0 commit comments