From 38252e6e610e563a7c03561174523914fff169bf Mon Sep 17 00:00:00 2001 From: Repro Date: Sun, 11 Oct 2026 23:22:05 +0800 Subject: [PATCH] docs(openapi): clarify response headers schema format and support object properties Co-Authored-By: Claude Code --- README.md | 26 +++++++++++++++ lib/spec/openapi/utils.js | 12 ++++--- lib/spec/swagger/utils.js | 4 ++- test/spec/openapi/schema.test.js | 54 ++++++++++++++++++++++++++++++++ test/spec/swagger/schema.test.js | 38 ++++++++++++++++++++++ 5 files changed, 129 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3f67c54f..f545c91b 100644 --- a/README.md +++ b/README.md @@ -717,6 +717,32 @@ Specify the `type` property when decorating response headers to prevent schema m } ``` +Response headers can also be declared using standard JSON Schema or TypeBox object structures containing `properties`: + +```js +{ + response: { + 200: { + type: 'object', + properties: { + id: { type: 'string' } + }, + headers: { + type: 'object', + properties: { + 'X-Total-Count': { + type: 'integer', + description: 'Total number of items' + } + } + } + } + } +} +``` + +In OpenAPI 3.0/3.1 specifications, response `headers` are mapped as key-value maps of Header Objects (with `schema` and optional `description`). When `headers` is passed as a top-level schema object with `properties`, `@fastify/swagger` unwraps the nested properties to populate the response headers map. + ##### Different content types responses > ℹ️ Note: Supported [only by OpenAPI v3](https://swagger.io/docs/specification/v3_0/describing-responses/), not Swagger (OpenAPI v2). diff --git a/lib/spec/openapi/utils.js b/lib/spec/openapi/utils.js index 8845a55c..1fdf7670 100644 --- a/lib/spec/openapi/utils.js +++ b/lib/spec/openapi/utils.js @@ -349,13 +349,17 @@ function resolveResponse (opts, fastifyResponseJson, produces, ref) { // add headers when there are any. if (rawJsonSchema.headers) { response.headers = {} - Object.keys(rawJsonSchema.headers).forEach(function (key) { + const rawHeaders = (rawJsonSchema.headers.properties && typeof rawJsonSchema.headers.properties === 'object') + ? rawJsonSchema.headers.properties + : rawJsonSchema.headers + + Object.keys(rawHeaders).forEach(function (key) { const header = { - schema: { ...rawJsonSchema.headers[key] } + schema: { ...rawHeaders[key] } } - if (rawJsonSchema.headers[key].description) { - header.description = rawJsonSchema.headers[key].description + if (rawHeaders[key].description) { + header.description = rawHeaders[key].description // remove invalid field delete header.schema.description } diff --git a/lib/spec/swagger/utils.js b/lib/spec/swagger/utils.js index af9db7ad..35782470 100644 --- a/lib/spec/swagger/utils.js +++ b/lib/spec/swagger/utils.js @@ -251,7 +251,9 @@ function resolveResponse (fastifyResponseJson, ref) { // add headers when there are any. if (rawJsonSchema.headers) { - response.headers = rawJsonSchema.headers + response.headers = (rawJsonSchema.headers.properties && typeof rawJsonSchema.headers.properties === 'object') + ? rawJsonSchema.headers.properties + : rawJsonSchema.headers // remove invalid field delete resolved.headers } diff --git a/test/spec/openapi/schema.test.js b/test/spec/openapi/schema.test.js index f514c58c..47a6e0fc 100644 --- a/test/spec/openapi/schema.test.js +++ b/test/spec/openapi/schema.test.js @@ -399,6 +399,60 @@ test('support response headers', async t => { t.assert.strictEqual(definedPath.responses['200'].content['application/json'].schema.headers, undefined) }) +test('support response headers defined as JSON schema object with properties', async t => { + const opt = { + schema: { + response: { + 200: { + type: 'object', + properties: { + hello: { + type: 'string' + } + }, + headers: { + type: 'object', + properties: { + 'X-WORLD': { + type: 'string' + }, + 'X-DESCRIPTION': { + description: 'Foo', + type: 'string' + } + } + } + } + } + } + } + + const fastify = Fastify() + await fastify.register(fastifySwagger, { + openapi: true + }) + fastify.get('/', opt, () => {}) + + await fastify.ready() + + const swaggerObject = fastify.swagger() + const api = await Swagger.validate(swaggerObject) + + const definedPath = api.paths['/'].get + t.assert.deepStrictEqual(definedPath.responses['200'].headers['X-WORLD'], { + schema: { + type: 'string' + } + }) + t.assert.deepStrictEqual(definedPath.responses['200'].headers['X-DESCRIPTION'], { + description: 'Foo', + schema: { + type: 'string' + } + }) + t.assert.strictEqual(definedPath.responses['200'].content['application/json'].schema.headers, undefined) +}) + test('response: description and x-response-description', async () => { const description = 'description - always that of response body, sometimes also that of response as a whole' const responseDescription = 'description only for the response as a whole' diff --git a/test/spec/swagger/schema.test.js b/test/spec/swagger/schema.test.js index 1adee53c..bfab480f 100644 --- a/test/spec/swagger/schema.test.js +++ b/test/spec/swagger/schema.test.js @@ -275,6 +275,44 @@ test('support response headers', async t => { t.assert.strictEqual(definedPath.responses['200'].schema.headers, undefined) }) +test('support response headers defined as JSON schema object with properties', async t => { + const opt = { + schema: { + response: { + 200: { + type: 'object', + properties: { + hello: { + type: 'string' + } + }, + headers: { + type: 'object', + properties: { + 'X-WORLD': { + type: 'string' + } + } + } + } + } + } + } + + const fastify = Fastify() + await fastify.register(fastifySwagger) + fastify.get('/', opt, () => {}) + + await fastify.ready() + + const swaggerObject = fastify.swagger() + const api = await Swagger.validate(swaggerObject) + + const definedPath = api.paths['/'].get + t.assert.deepStrictEqual(definedPath.responses['200'].headers, opt.schema.response['200'].headers.properties) + t.assert.strictEqual(definedPath.responses['200'].schema.headers, undefined) +}) + test('response: description and x-response-description', async () => { const description = 'description - always that of response body, sometimes also that of response as a whole' const responseDescription = 'description only for the response as a whole'