diff --git a/README.md b/README.md
index ffbd858e..585676ae 100644
--- a/README.md
+++ b/README.md
@@ -726,6 +726,140 @@ You can integration this plugin with ```@fastify/helmet``` with some little work
})
```
+
+### 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)
+ }
+})
+```
+
## `$id` and `$ref` usage
diff --git a/lib/constants.js b/lib/constants.js
index 00c76c8d..d35a9549 100644
--- a/lib/constants.js
+++ b/lib/constants.js
@@ -2,8 +2,10 @@
const xConsume = 'x-consume'
const xResponseDescription = 'x-response-description'
+const xExamples = 'x-examples'
module.exports = {
xConsume,
- xResponseDescription
+ xResponseDescription,
+ xExamples
}
diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js
index a19466d4..5583022d 100644
--- a/lib/spec/openapi/utils.js
+++ b/lib/spec/openapi/utils.js
@@ -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) {
@@ -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
@@ -167,12 +167,15 @@ 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
@@ -180,7 +183,7 @@ function plainJsonObjectToOpenapi3 (container, jsonSchema, externalSchemas, secu
break
case 'header':
toOpenapiProp = function (propertyName, jsonSchemaElement) {
- return {
+ const result = {
in: 'header',
name: propertyName,
required: jsonSchemaElement.required,
@@ -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
}
@@ -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
}
@@ -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
diff --git a/test/spec/openapi/option.js b/test/spec/openapi/option.js
index a79c5a6d..349f08af 100644
--- a/test/spec/openapi/option.js
+++ b/test/spec/openapi/option.js
@@ -393,7 +393,64 @@ test('transforms examples in example if single object example', async (t) => {
t.same(schema.properties.hello.example, { lorem: 'ipsum' })
})
-test('copy example from component to media', async (t) => {
+test('move examples from "x-examples" to examples field', async (t) => {
+ t.plan(3)
+ const fastify = Fastify({
+ ajv: {
+ plugins: [
+ function (ajv) {
+ ajv.addKeyword({ keyword: 'x-examples' })
+ }
+ ]
+ }
+ })
+
+ await fastify.register(fastifySwagger, openapiOption)
+
+ const opts = {
+ schema: {
+ body: {
+ type: 'object',
+ required: ['hello'],
+ properties: {
+ hello: {
+ type: 'object',
+ properties: {
+ lorem: {
+ type: 'string'
+ }
+ }
+ }
+ },
+ 'x-examples': {
+ 'lorem ipsum': {
+ summary: 'Roman statesman',
+ value: { lorem: 'ipsum' }
+ }
+ }
+ }
+ }
+ }
+
+ fastify.post('/', opts, () => {})
+
+ await fastify.ready()
+
+ const openapiObject = fastify.swagger()
+ const content = openapiObject.paths['/'].post.requestBody.content['application/json']
+ const schema = content.schema
+
+ t.ok(schema)
+ t.notOk(schema['x-examples'])
+ t.same(content.examples, {
+ 'lorem ipsum': {
+ summary: 'Roman statesman',
+ value: { lorem: 'ipsum' }
+ }
+ })
+})
+
+test('copy example of body from component to media', async (t) => {
t.plan(4)
const fastify = Fastify()
@@ -429,7 +486,92 @@ test('copy example from component to media', async (t) => {
t.same(content.example, { hello: 'world' })
})
-test('move examples from component to media', async (t) => {
+test('copy example of response from component to media', async (t) => {
+ t.plan(4)
+ const fastify = Fastify()
+
+ await fastify.register(fastifySwagger, openapiOption)
+
+ const response = {
+ type: 'object',
+ properties: {
+ hello: {
+ type: 'string'
+ }
+ },
+ examples: [{ hello: 'world' }]
+ }
+
+ const opts = {
+ schema: {
+ response: { 200: response }
+ }
+ }
+
+ fastify.post('/', opts, () => {})
+
+ await fastify.ready()
+
+ const openapiObject = fastify.swagger()
+ const content = openapiObject.paths['/'].post.responses['200'].content['application/json']
+ const schema = content.schema
+
+ t.ok(schema)
+ t.ok(schema.properties)
+ t.same(schema.example, { hello: 'world' })
+ t.same(content.example, { hello: 'world' })
+})
+
+test('copy example of parameters from component to media', async (t) => {
+ t.plan(7)
+ const fastify = Fastify()
+
+ await fastify.register(fastifySwagger, openapiOption)
+
+ const portSchema = {
+ type: 'number',
+ examples: [8080]
+ }
+
+ const opts = {
+ schema: {
+ headers: {
+ 'X-Port': portSchema
+ },
+ querystring: {
+ port: portSchema
+ },
+ params: {
+ port: portSchema
+ }
+ }
+ }
+
+ fastify.post('/:port', opts, () => {})
+
+ await fastify.ready()
+
+ const openapiObject = fastify.swagger()
+ const parameters = openapiObject.paths['/{port}'].post.parameters
+
+ t.ok(parameters)
+
+ const paramsMap = new Map(parameters.map(param => [param.in, param]))
+
+ const headerParam = paramsMap.get('header')
+ t.ok(headerParam)
+ t.same(headerParam.example, 8080)
+
+ const queryParam = paramsMap.get('query')
+ t.ok(queryParam)
+ t.same(queryParam.example, 8080)
+
+ const pathParam = paramsMap.get('path')
+ t.ok(pathParam)
+ t.same(pathParam.example, 8080)
+})
+
+test('move examples of body from component to media', async (t) => {
t.plan(4)
const fastify = Fastify()
@@ -465,6 +607,96 @@ test('move examples from component to media', async (t) => {
t.same(content.examples, { example1: { value: { hello: 'world' } }, example2: { value: { hello: 'lorem' } } })
})
+test('move examples of response from component to media', async (t) => {
+ t.plan(4)
+ const fastify = Fastify()
+
+ await fastify.register(fastifySwagger, openapiOption)
+
+ const response = {
+ type: 'object',
+ properties: {
+ hello: {
+ type: 'string'
+ }
+ },
+ examples: [{ hello: 'world' }, { hello: 'lorem' }]
+ }
+
+ const opts = {
+ schema: {
+ response: { 200: response }
+ }
+ }
+
+ fastify.post('/', opts, () => {})
+
+ await fastify.ready()
+
+ const openapiObject = fastify.swagger()
+ const content = openapiObject.paths['/'].post.responses['200'].content['application/json']
+ const schema = content.schema
+
+ t.ok(schema)
+ t.ok(schema.properties)
+ t.notOk(schema.examples)
+ t.same(content.examples, { example1: { value: { hello: 'world' } }, example2: { value: { hello: 'lorem' } } })
+})
+
+test('move examples of parameters from component to media', async (t) => {
+ t.plan(7)
+ const fastify = Fastify()
+
+ await fastify.register(fastifySwagger, openapiOption)
+
+ const portSchema = {
+ type: 'number',
+ examples: [8080, 80]
+ }
+
+ const opts = {
+ schema: {
+ headers: {
+ 'X-Port': portSchema
+ },
+ querystring: {
+ port: portSchema
+ },
+ params: {
+ port: portSchema
+ }
+ }
+ }
+
+ fastify.post('/:port', opts, () => {})
+
+ await fastify.ready()
+
+ const openapiObject = fastify.swagger()
+ const parameters = openapiObject.paths['/{port}'].post.parameters
+
+ t.ok(parameters)
+
+ const paramsMap = new Map(parameters.map(param => [param.in, param]))
+
+ const expectedExamples = {
+ 80: { value: 80 },
+ 8080: { value: 8080 }
+ }
+
+ const headerParam = paramsMap.get('header')
+ t.ok(headerParam)
+ t.same(headerParam.examples, expectedExamples)
+
+ const queryParam = paramsMap.get('query')
+ t.ok(queryParam)
+ t.same(queryParam.examples, expectedExamples)
+
+ const pathParam = paramsMap.get('path')
+ t.ok(pathParam)
+ t.same(pathParam.examples, expectedExamples)
+})
+
test('uses examples if has multiple string examples', async (t) => {
t.plan(3)
const fastify = Fastify()