Skip to content

Commit 815d783

Browse files
mxckEomm
andauthored
Allow custom schema examples in OpenAPI format (#616)
* Custom schema examples * Examples field only valid in media object * linting * Update README.md Co-authored-by: Manuel Spigolon <behemoth89@gmail.com> * Fix example * Support examples in response * Examples in params * Fix typo * Update readme Co-authored-by: Manuel Spigolon <behemoth89@gmail.com>
1 parent 04a3d16 commit 815d783

4 files changed

Lines changed: 425 additions & 32 deletions

File tree

README.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,140 @@ You can integration this plugin with ```@fastify/helmet``` with some little work
726726
})
727727
```
728728
729+
<a name="schema.examplesField"></a>
730+
### Add examples to the schema
731+
732+
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.
733+
734+
Array with examples from JSON Schema converted to OpenAPI `example` or `examples` field automatically with generated names (example1, example2...):
735+
736+
```js
737+
fastify.route({
738+
method: 'POST',
739+
url: '/',
740+
schema: {
741+
querystring: {
742+
type: 'object',
743+
required: ['filter'],
744+
properties: {
745+
filter: {
746+
type: 'object',
747+
required: ['foo'],
748+
properties: {
749+
foo: { type: 'string' },
750+
bar: { type: 'string' }
751+
},
752+
examples: [
753+
{ foo: 'bar', bar: 'baz' },
754+
{ foo: 'foo', bar: 'bar' }
755+
]
756+
}
757+
},
758+
examples: [
759+
{ filter: { foo: 'bar', bar: 'baz' } }
760+
]
761+
}
762+
},
763+
handler (request, reply) {
764+
reply.send(request.query.filter)
765+
}
766+
})
767+
```
768+
769+
Will generate this in the OpenAPI v3 schema's `path`:
770+
771+
```json
772+
"/": {
773+
"post": {
774+
"requestBody": {
775+
"content": {
776+
"application/json": {
777+
"schema": {
778+
"type": "object",
779+
"required": ["filter"],
780+
"properties": {
781+
"filter": {
782+
"type": "object",
783+
"required": ["foo"],
784+
"properties": {
785+
"foo": { "type": "string" },
786+
"bar": { "type": "string" }
787+
},
788+
"example": { "foo": "bar", "bar": "baz" }
789+
}
790+
}
791+
},
792+
"examples": {
793+
"example1": {
794+
"value": { "filter": { "foo": "bar", "bar": "baz" } }
795+
},
796+
"example2": {
797+
"value": { "filter": { "foo": "foo", "bar": "bar" } }
798+
}
799+
}
800+
}
801+
},
802+
"required": true
803+
},
804+
"responses": { "200": { "description": "Default Response" } }
805+
}
806+
}
807+
```
808+
809+
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):
810+
811+
```js
812+
// Need to add a new allowed keyword to ajv in fastify instance
813+
const fastify = Fastify({
814+
ajv: {
815+
plugins: [
816+
function (ajv) {
817+
ajv.addKeyword({ keyword: 'x-examples' })
818+
}
819+
]
820+
}
821+
})
822+
823+
fastify.route({
824+
method: 'POST',
825+
url: '/feed-animals',
826+
schema: {
827+
body: {
828+
type: 'object',
829+
required: ['animals'],
830+
properties: {
831+
animals: {
832+
type: 'array',
833+
items: {
834+
type: 'string'
835+
},
836+
minItems: 1,
837+
}
838+
},
839+
"x-examples": {
840+
Cats: {
841+
summary: "Feed cats",
842+
description:
843+
"A longer **description** of the options with cats",
844+
value: {
845+
animals: ["Tom", "Garfield", "Felix"]
846+
}
847+
},
848+
Dogs: {
849+
summary: "Feed dogs",
850+
value: {
851+
animals: ["Spike", "Odie", "Snoopy"]
852+
}
853+
}
854+
}
855+
}
856+
},
857+
handler (request, reply) {
858+
reply.send(request.body.animals)
859+
}
860+
})
861+
```
862+
729863
<a name="usage"></a>
730864
## `$id` and `$ref` usage
731865

lib/constants.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
const xConsume = 'x-consume'
44
const xResponseDescription = 'x-response-description'
5+
const xExamples = 'x-examples'
56

67
module.exports = {
78
xConsume,
8-
xResponseDescription
9+
xResponseDescription,
10+
xExamples
911
}

lib/spec/openapi/utils.js

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use strict'
22

33
const { readPackageJson, formatParamUrl, resolveLocalRef } = require('../../util/common')
4-
const { xResponseDescription, xConsume } = require('../../constants')
4+
const { xResponseDescription, xConsume, xExamples } = require('../../constants')
55
const { rawRequired } = require('../../symbols')
66

77
function prepareDefaultOptions (opts) {
@@ -136,26 +136,26 @@ function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, secu
136136
case 'cookie':
137137
case 'query':
138138
toOpenapiProp = function (propertyName, jsonSchemaElement) {
139-
const result = {
139+
let result = {
140140
in: container,
141141
name: propertyName,
142142
required: jsonSchemaElement.required
143143
}
144+
145+
const media = schemaToMedia(jsonSchemaElement)
146+
144147
// complex serialization in query or cookie, eg. JSON
145148
// https://swagger.io/docs/specification/describing-parameters/#schema-vs-content
146149
if (jsonSchemaElement[xConsume]) {
150+
media.schema.required = jsonSchemaElement[rawRequired]
151+
147152
result.content = {
148-
[jsonSchemaElement[xConsume]]: {
149-
schema: {
150-
...jsonSchemaElement,
151-
required: jsonSchemaElement[rawRequired]
152-
}
153-
}
153+
[jsonSchemaElement[xConsume]]: media
154154
}
155155

156156
delete result.content[jsonSchemaElement[xConsume]].schema[xConsume]
157157
} else {
158-
result.schema = jsonSchemaElement
158+
result = { ...media, ...result }
159159
}
160160
// description should be optional
161161
if (jsonSchemaElement.description) result.description = jsonSchemaElement.description
@@ -167,20 +167,23 @@ function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, secu
167167
break
168168
case 'path':
169169
toOpenapiProp = function (propertyName, jsonSchemaElement) {
170+
const media = schemaToMedia(jsonSchemaElement)
171+
170172
const result = {
173+
...media,
171174
in: container,
172175
name: propertyName,
173-
required: true,
174-
schema: jsonSchemaElement
176+
required: true
175177
}
178+
176179
// description should be optional
177180
if (jsonSchemaElement.description) result.description = jsonSchemaElement.description
178181
return result
179182
}
180183
break
181184
case 'header':
182185
toOpenapiProp = function (propertyName, jsonSchemaElement) {
183-
return {
186+
const result = {
184187
in: 'header',
185188
name: propertyName,
186189
required: jsonSchemaElement.required,
@@ -189,6 +192,17 @@ function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, secu
189192
type: jsonSchemaElement.type
190193
}
191194
}
195+
196+
const media = schemaToMedia(jsonSchemaElement)
197+
if (media.example) {
198+
result.example = media.example
199+
}
200+
201+
if (media.examples) {
202+
result.examples = media.examples
203+
}
204+
205+
return result
192206
}
193207
break
194208
}
@@ -207,28 +221,39 @@ function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, secu
207221
})
208222
}
209223

224+
function schemaToMedia (schema) {
225+
const media = { schema }
226+
227+
if (schema.examples) {
228+
media.examples = schema.examples
229+
230+
// examples is invalid property of media object schema
231+
delete schema.examples
232+
}
233+
234+
if (schema.example) {
235+
media.example = schema.example
236+
}
237+
238+
if (schema[xExamples]) {
239+
media.examples = schema[xExamples]
240+
delete schema[xExamples]
241+
}
242+
243+
return media
244+
}
245+
210246
function resolveBodyParams (body, schema, consumes, ref) {
211247
const resolved = transformDefsToComponents(ref.resolve(schema))
212248
if ((Array.isArray(consumes) && consumes.length === 0) || typeof consumes === 'undefined') {
213249
consumes = ['application/json']
214250
}
215251

252+
const media = schemaToMedia(resolved)
216253
consumes.forEach((consume) => {
217-
// examples and example fields should be on the top level of the media object
218-
const mediaObject = { schema: resolved }
219-
if (resolved.examples) {
220-
mediaObject.examples = resolved.examples
221-
222-
// examples is invalid property of media object schema
223-
delete resolved.examples
224-
}
225-
226-
if (resolved.example) {
227-
mediaObject.example = resolved.example
228-
}
229-
230-
body.content[consume] = mediaObject
254+
body.content[consume] = media
231255
})
256+
232257
if (resolved && resolved.required && resolved.required.length) {
233258
body.required = true
234259
}
@@ -305,10 +330,10 @@ function resolveResponse (fastifyResponseJson, produces, ref) {
305330
}
306331

307332
delete resolved[xResponseDescription]
333+
334+
const media = schemaToMedia(resolved)
308335
produces.forEach((produce) => {
309-
content[produce] = {
310-
schema: resolved
311-
}
336+
content[produce] = media
312337
})
313338

314339
response.content = content

0 commit comments

Comments
 (0)