diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index 5583022d..509e1561 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -99,7 +99,10 @@ function transformDefsToComponents (jsonSchema) { jsonSchema[key][prop] = transformDefsToComponents(jsonSchema[key][prop]) }) } else if (key === '$ref') { + // replace the top-lvl path jsonSchema[key] = jsonSchema[key].replace('definitions', 'components/schemas') + // replace the path for nested defs + jsonSchema[key] = jsonSchema[key].replaceAll('definitions', 'properties') } else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length > 1)) { jsonSchema.examples = convertExamplesArrayToObject(jsonSchema.examples) } else if (key === 'examples' && Array.isArray(jsonSchema[key]) && (jsonSchema[key].length === 1)) { @@ -402,16 +405,34 @@ function prepareOpenapiSchemas (schemas, ref) { return Object.entries(schemas) .reduce((res, [name, schema]) => { const _ = { ...schema } - const resolved = transformDefsToComponents(ref.resolve(_, { externalSchemas: [schemas] })) - // Swagger doesn't accept $id on /definitions schemas. - // The $ids are needed by Ref() to check the URI so we need - // to remove them at the end of the process - // definitions are added by resolve but they are replace by components.schemas - delete resolved.$id - delete resolved.definitions + // 'definitions' keyword is not supported by openapi in schema item + // but we can receive it from json-schema input + if (_.definitions) { + _.properties = { + ..._.properties, + ..._.definitions + } + } + + // ref.resolve call does 3 things: + // modifies underlying cache of ref + // adds 'definitions' with resolved schema(which we don't need here anymore) + // mutates $ref to point to the resolved schema + // ($ref will be mutated again by transformDefsToComponents) + const resolvedRef = ref.resolve(_, { externalSchemas: [schemas] }) + + // swagger doesn't accept $id on components schemas + // $ids are needed by ref.resolve to check the URI + // definitions are added by resolve, but they are not needed, as we resolve + // the $ref to already existing schemas in components.schemas using method below, not the definition ones + // therefore, we delete both $id and definitions at the end of the process + delete resolvedRef.$id + delete resolvedRef.definitions + + const components = transformDefsToComponents(resolvedRef) - res[name] = resolved + res[name] = components return res }, {}) } diff --git a/test/spec/openapi/refs.js b/test/spec/openapi/refs.js index a7d758ef..0481385f 100644 --- a/test/spec/openapi/refs.js +++ b/test/spec/openapi/refs.js @@ -282,3 +282,42 @@ test('uses examples if has property required in body', async (t) => { t.ok(schema.parameters) t.same(schema.parameters[0].in, 'query') }) + +test('support schema with definitions keyword and $ref inside', async (t) => { + const fastify = Fastify() + + await fastify.register(fastifySwagger, openapiOption) + fastify.register(async (instance) => { + instance.addSchema({ + $id: 'NestedSchema', + definitions: { + SchemaA: { + type: 'object', + properties: { + id: { type: 'string' } + } + }, + SchemaB: { + type: 'object', + properties: { + example: { $ref: 'NestedSchema#/definitions/SchemaA' } + } + } + } + }) + instance.post('/url1', { schema: { body: { $ref: 'NestedSchema#/definitions/SchemaB' }, response: { 200: { $ref: 'NestedSchema#/definitions/SchemaA' } } } }, () => {}) + }) + + await fastify.ready() + + const openapiObject = fastify.swagger() + t.equal(typeof openapiObject, 'object') + t.match(Object.keys(openapiObject.components.schemas), ['NestedSchema']) + t.match(Object.keys(openapiObject.components.schemas.NestedSchema), ['properties']) + t.match(Object.keys(openapiObject.components.schemas.NestedSchema.properties), ['SchemaA', 'SchemaB']) + + // ref must be prefixed by '#/components/schemas/' + t.equal(openapiObject.components.schemas.NestedSchema.properties.SchemaB.properties.example.$ref, '#/components/schemas/NestedSchema/properties/SchemaA') + + await Swagger.validate(openapiObject) +})