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
134 changes: 134 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,140 @@ You can integration this plugin with ```@fastify/helmet``` with some little work
})
```

<a name="schema.examplesField"></a>
### Add examples to the schema

Note: [OpenAPI](https://swagger.io/specification/#example-object) and [JSON Schema](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.9.5) have different examples field formats.

Array with examples from JSON Schema converted to OpenAPI `example` or `examples` field automatically with generated names (example1, example2...):

```js
fastify.route({
method: 'POST',
url: '/',
schema: {
querystring: {
type: 'object',
required: ['filter'],
properties: {
filter: {
type: 'object',
required: ['foo'],
properties: {
foo: { type: 'string' },
bar: { type: 'string' }
},
examples: [
{ foo: 'bar', bar: 'baz' },
{ foo: 'foo', bar: 'bar' }
]
}
},
examples: [
{ filter: { foo: 'bar', bar: 'baz' } }
]
}
},
handler (request, reply) {
reply.send(request.query.filter)
}
})
```

Will generate this in the OpenAPI v3 schema's `path`:

```json
"/": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["filter"],
"properties": {
"filter": {
"type": "object",
"required": ["foo"],
"properties": {
"foo": { "type": "string" },
"bar": { "type": "string" }
},
"example": { "foo": "bar", "bar": "baz" }
}
}
},
"examples": {
"example1": {
"value": { "filter": { "foo": "bar", "bar": "baz" } }
},
"example2": {
"value": { "filter": { "foo": "foo", "bar": "bar" } }
}
}
}
},
"required": true
},
"responses": { "200": { "description": "Default Response" } }
}
}
```

If you want to set your own names or add descriptions to the examples of schemas, you can use `x-examples` field to set examples in [OpenAPI format](https://swagger.io/specification/#example-object):

```js
// Need to add a new allowed keyword to ajv in fastify instance
const fastify = Fastify({
ajv: {
plugins: [
function (ajv) {
ajv.addKeyword({ keyword: 'x-examples' })
}
]
}
})

fastify.route({
method: 'POST',
url: '/feed-animals',
schema: {
body: {
type: 'object',
required: ['animals'],
properties: {
animals: {
type: 'array',
items: {
type: 'string'
},
minItems: 1,
}
},
"x-examples": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you make this example complete? I think we need a custom Ajv config, i.e. ajv.addKeyword() to make this work.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Cats: {
summary: "Feed cats",
description:
"A longer **description** of the options with cats",
value: {
animals: ["Tom", "Garfield", "Felix"]
}
},
Dogs: {
summary: "Feed dogs",
value: {
animals: ["Spike", "Odie", "Snoopy"]
}
}
}
}
},
handler (request, reply) {
reply.send(request.body.animals)
}
})
```

<a name="usage"></a>
## `$id` and `$ref` usage

Expand Down
4 changes: 3 additions & 1 deletion lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

const xConsume = 'x-consume'
const xResponseDescription = 'x-response-description'
const xExamples = 'x-examples'

module.exports = {
xConsume,
xResponseDescription
xResponseDescription,
xExamples
}
83 changes: 54 additions & 29 deletions lib/spec/openapi/utils.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict'

const { readPackageJson, formatParamUrl, resolveLocalRef } = require('../../util/common')
const { xResponseDescription, xConsume } = require('../../constants')
const { xResponseDescription, xConsume, xExamples } = require('../../constants')
const { rawRequired } = require('../../symbols')

function prepareDefaultOptions (opts) {
Expand Down Expand Up @@ -136,26 +136,26 @@ function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, secu
case 'cookie':
case 'query':
toOpenapiProp = function (propertyName, jsonSchemaElement) {
const result = {
let result = {
in: container,
name: propertyName,
required: jsonSchemaElement.required
}

const media = schemaToMedia(jsonSchemaElement)

// complex serialization in query or cookie, eg. JSON
// https://swagger.io/docs/specification/describing-parameters/#schema-vs-content
if (jsonSchemaElement[xConsume]) {
media.schema.required = jsonSchemaElement[rawRequired]

result.content = {
[jsonSchemaElement[xConsume]]: {
schema: {
...jsonSchemaElement,
required: jsonSchemaElement[rawRequired]
}
}
[jsonSchemaElement[xConsume]]: media
}

delete result.content[jsonSchemaElement[xConsume]].schema[xConsume]
} else {
result.schema = jsonSchemaElement
result = { ...media, ...result }
}
// description should be optional
if (jsonSchemaElement.description) result.description = jsonSchemaElement.description
Expand All @@ -167,20 +167,23 @@ function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, secu
break
case 'path':
toOpenapiProp = function (propertyName, jsonSchemaElement) {
const media = schemaToMedia(jsonSchemaElement)

const result = {
...media,
in: container,
name: propertyName,
required: true,
schema: jsonSchemaElement
required: true
}

// description should be optional
if (jsonSchemaElement.description) result.description = jsonSchemaElement.description
return result
}
break
case 'header':
toOpenapiProp = function (propertyName, jsonSchemaElement) {
return {
const result = {
in: 'header',
name: propertyName,
required: jsonSchemaElement.required,
Expand All @@ -189,6 +192,17 @@ function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, secu
type: jsonSchemaElement.type
}
}

const media = schemaToMedia(jsonSchemaElement)
if (media.example) {
result.example = media.example
}

if (media.examples) {
result.examples = media.examples
}

return result
}
break
}
Expand All @@ -207,28 +221,39 @@ function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, secu
})
}

function schemaToMedia (schema) {
const media = { schema }

if (schema.examples) {
media.examples = schema.examples

// examples is invalid property of media object schema
delete schema.examples
}

if (schema.example) {
media.example = schema.example
}

if (schema[xExamples]) {
media.examples = schema[xExamples]
delete schema[xExamples]
}

return media
}

function resolveBodyParams (body, schema, consumes, ref) {
const resolved = transformDefsToComponents(ref.resolve(schema))
if ((Array.isArray(consumes) && consumes.length === 0) || typeof consumes === 'undefined') {
consumes = ['application/json']
}

const media = schemaToMedia(resolved)
consumes.forEach((consume) => {
// examples and example fields should be on the top level of the media object
const mediaObject = { schema: resolved }
if (resolved.examples) {
mediaObject.examples = resolved.examples

// examples is invalid property of media object schema
delete resolved.examples
}

if (resolved.example) {
mediaObject.example = resolved.example
}

body.content[consume] = mediaObject
body.content[consume] = media
})

if (resolved && resolved.required && resolved.required.length) {
body.required = true
}
Expand Down Expand Up @@ -305,10 +330,10 @@ function resolveResponse (fastifyResponseJson, produces, ref) {
}

delete resolved[xResponseDescription]

const media = schemaToMedia(resolved)
produces.forEach((produce) => {
content[produce] = {
schema: resolved
}
content[produce] = media
})

response.content = content
Expand Down
Loading