Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,29 @@ await fastify.register(require('@fastify/swagger'), {

For details on `buildLocalReference` arguments, see the [documentation](https://github.com/Eomm/json-schema-resolver#usage-resolve-one-schema-against-external-schemas).

##### `definitions` and `$defs` of a shared schema

Swagger and OpenAPI do not allow the `definitions` and `$defs` keywords inside a schema object.
The definitions nested into a schema added with `fastify.addSchema()` are moved next to it, named `<schema>-<key>`
(a numeric suffix is appended when the name is already taken), and the `$ref`s pointing to them are updated:

```js
fastify.addSchema({
$id: 'http://example.com/common.json',
type: 'object',
definitions: {
address: { $id: '#address', type: 'object', properties: { city: { type: 'string' } } }
}
})

// both are rendered as `#/components/schemas/def-0-address` (`#/definitions/def-0-address` with Swagger)
{ $ref: 'http://example.com/common.json#/definitions/address' }
{ $ref: 'http://example.com/common.json#address' }
```

A reference to an anchor (a fragment-only `$id` like `#address`) must be written as the `$id` of the shared schema
followed by the anchor, or as `#address` from inside the shared schema itself.

<a name="register.options.decorator"></a>
#### Decorator

Expand Down
6 changes: 5 additions & 1 deletion lib/spec/openapi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const yaml = require('yaml')
const { shouldRouteHide } = require('../../util/should-route-hide')
const { rewriteHoistedRefs } = require('../../util/definitions')
const { addOpenapiOperation, prepareDefaultOptions, prepareOpenapiObject, prepareOpenapiMethod, prepareOpenapiSchemas, normalizeUrl, resolveServerUrls } = require('./utils')

module.exports = function (opts, cache, routes, Ref) {
Expand All @@ -20,10 +21,11 @@ module.exports = function (opts, cache, routes, Ref) {
const openapiObject = prepareOpenapiObject(defOpts)

ref = Ref()
const hoisted = new Map()
openapiObject.components.schemas = prepareOpenapiSchemas(defOpts, {
...openapiObject.components.schemas,
...(ref.definitions().definitions)
}, ref)
}, ref, hoisted)

const serverUrls = resolveServerUrls(defOpts.servers)

Expand Down Expand Up @@ -70,6 +72,8 @@ module.exports = function (opts, cache, routes, Ref) {
openapiObject.paths[url] = openapiRoute
}

rewriteHoistedRefs(openapiObject, '#/components/schemas/', hoisted)

const transformObjectResult = defOpts.transformObject
? defOpts.transformObject({ openapiObject })
: openapiObject
Expand Down
17 changes: 12 additions & 5 deletions lib/spec/openapi/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const { readPackageJson } = require('../../util/read-package-json')
const { formatParamUrl } = require('../../util/format-param-url')
const { resolveLocalRef } = require('../../util/resolve-local-ref')
const { hoistDefinitions, unknownSchemas } = require('../../util/definitions')
const { resolveSchemaReference } = require('../../util/resolve-schema-reference')
const { xResponseDescription, xConsume, xExamples } = require('../../constants')
const { rawRequired } = require('../../symbols')
Expand Down Expand Up @@ -686,17 +687,23 @@ function convertJsonSchemaToOpenapi3 (opts, jsonSchema) {
return openapiSchema
}

function prepareOpenapiSchemas (opts, jsonSchemas, ref) {
function prepareOpenapiSchemas (opts, jsonSchemas, ref, hoisted = new Map()) {
const openapiSchemas = {}
const externalSchemas = [unknownSchemas(jsonSchemas, ref)]

for (const schemaName of Object.keys(jsonSchemas)) {
const jsonSchema = { ...jsonSchemas[schemaName] }

const resolvedJsonSchema = ref.resolve(jsonSchema, { externalSchemas: [jsonSchemas] })
const openapiSchema = convertJsonSchemaToOpenapi3(opts, resolvedJsonSchema)
resolveSchemaExamplesRecursive(openapiSchema)
const resolvedJsonSchema = ref.resolve(jsonSchema, { externalSchemas })
// OpenAPI does not support the `definitions` keyword: before it gets
// dropped by the conversion, the definitions are moved to the top-level
const hoistedSchemas = hoistDefinitions(schemaName, resolvedJsonSchema, jsonSchemas, hoisted)

openapiSchemas[schemaName] = openapiSchema
for (const [name, schema] of [[schemaName, resolvedJsonSchema], ...hoistedSchemas]) {
const openapiSchema = convertJsonSchemaToOpenapi3(opts, schema)
resolveSchemaExamplesRecursive(openapiSchema)
openapiSchemas[name] = openapiSchema
}
}
return openapiSchemas
}
Expand Down
6 changes: 5 additions & 1 deletion lib/spec/swagger/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const yaml = require('yaml')
const { shouldRouteHide } = require('../../util/should-route-hide')
const { rewriteHoistedRefs } = require('../../util/definitions')
const { prepareDefaultOptions, prepareSwaggerObject, prepareSwaggerMethod, normalizeUrl, prepareSwaggerDefinitions } = require('./utils')

module.exports = function (opts, cache, routes, Ref) {
Expand All @@ -19,10 +20,11 @@ module.exports = function (opts, cache, routes, Ref) {
const swaggerObject = prepareSwaggerObject(defOpts)

ref = Ref()
const hoisted = new Map()
swaggerObject.definitions = prepareSwaggerDefinitions({
...swaggerObject.definitions,
...(ref.definitions().definitions)
}, ref)
}, ref, hoisted)

for (const route of routes) {
const transformResult = route.config?.swaggerTransform !== undefined
Expand Down Expand Up @@ -62,6 +64,8 @@ module.exports = function (opts, cache, routes, Ref) {
swaggerObject.paths[url] = swaggerRoute
}

rewriteHoistedRefs(swaggerObject, '#/definitions/', hoisted)

const transformObjectResult = defOpts.transformObject
? defOpts.transformObject({ swaggerObject })
: swaggerObject
Expand Down
13 changes: 10 additions & 3 deletions lib/spec/swagger/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const { readPackageJson } = require('../../util/read-package-json')
const { formatParamUrl } = require('../../util/format-param-url')
const { resolveLocalRef } = require('../../util/resolve-local-ref')
const { hoistDefinitions, unknownSchemas } = require('../../util/definitions')
const { resolveSchemaReference } = require('../../util/resolve-schema-reference')
const { xResponseDescription, xConsume } = require('../../constants')
const { generateParamsSchema } = require('../../util/generate-params-schema')
Expand Down Expand Up @@ -329,19 +330,25 @@ function prepareSwaggerMethod (schema, ref, swaggerObject, url) {
return swaggerMethod
}

function prepareSwaggerDefinitions (definitions, ref) {
function prepareSwaggerDefinitions (definitions, ref, hoisted = new Map()) {
const externalSchemas = [unknownSchemas(definitions, ref)]
return Object.entries(definitions)
.reduce((res, [name, definition]) => {
const _ = { ...definition }
const resolved = ref.resolve(_, { externalSchemas: [definitions] })
const resolved = ref.resolve(_, { externalSchemas })

// 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
delete resolved.$id
delete resolved.definitions

// Swagger doesn't accept the `definitions` keyword either:
// the nested definitions are moved to the top-level ones
res[name] = resolved
for (const [hoistedName, schema] of hoistDefinitions(name, resolved, definitions, hoisted)) {
delete schema.$id
res[hoistedName] = schema
}
return res
}, {})
}
Expand Down
14 changes: 13 additions & 1 deletion lib/util/add-hook.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const Ref = require('json-schema-resolver')
const cloner = require('rfdc')({ proto: true, circles: false })
const { prepareSharedSchemas, rewriteAnchorRefs } = require('./definitions')

function addHook (fastify, pluginOptions) {
const routes = []
Expand Down Expand Up @@ -69,11 +70,22 @@ function addHook (fastify, pluginOptions) {
throw new Error('.swagger() must be called after .ready()')
}
const externalSchemas = cloner(Array.from(sharedSchemasMap.values()))
return Ref(Object.assign(
const anchors = prepareSharedSchemas(externalSchemas)

const ref = Ref(Object.assign(
{ applicationUri: 'todo.com' },
pluginOptions.refResolver,
{ clone: true, externalSchemas })
)
if (anchors.size === 0) return ref

// The ref resolver does not support the references to an anchor
// (`http://foo/common.json#address`): they are converted to JSON pointers
// before the resolution. The clone avoids touching the route schemas.
return {
definitions: ref.definitions,
resolve: (schema, opts) => ref.resolve(rewriteAnchorRefs(cloner(schema), anchors), opts)
}
}
}
}
Expand Down
Loading
Loading