Skip to content

Commit

Permalink
Allow custom schema examples in OpenAPI format (#616)
Browse files Browse the repository at this point in the history
* Custom schema examples

* Examples field only valid in media object

* linting

* Update README.md

Co-authored-by: Manuel Spigolon <[email protected]>

* Fix example

* Support examples in response

* Examples in params

* Fix typo

* Update readme

Co-authored-by: Manuel Spigolon <[email protected]>
  • Loading branch information
mxck and Eomm authored Jun 19, 2022
1 parent 04a3d16 commit 815d783
Show file tree
Hide file tree
Showing 4 changed files with 425 additions and 32 deletions.
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": {
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

0 comments on commit 815d783

Please sign in to comment.