openapi-docs: support reusable parameters and response headers in components (#1411) - #5478
Draft
magdzikk wants to merge 11 commits into
Draft
openapi-docs: support reusable parameters and response headers in components (#1411)#5478magdzikk wants to merge 11 commits into
magdzikk wants to merge 11 commits into
Conversation
…cateComponentName
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Introduces a .reusableComponent marker for Tapir OpenAPI docs generation so that selected parameters and response headers are emitted once under components and referenced via $ref from operations/responses.
Changes:
- Add reusable components pre-pass, component emission, and
$ref-based reuse for marked parameters and response headers. - Add
failOnDuplicateComponentNameoption (defaulttrue) and improve de-duplication error messaging via a generalizedcalculateUniqueIds. - Add test coverage + YAML fixtures documenting expected OpenAPI output and duplicate-name behavior.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/VerifyYamlReusableComponentsTest.scala | Verifies YAML output uses $ref for reusable parameters/headers and omits components when unmarked. |
| docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/ReusableComponentsForEndpointsTest.scala | Unit tests for reusable component collection and duplicate-name handling. |
| docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/ReusableComponentAttributeTest.scala | Tests the new .reusableComponent attribute marker API and type preservation. |
| docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/EndpointToParametersTest.scala | Tests shared parameter conversion logic to keep emitted parameters consistent. |
| docs/openapi-docs/src/test/scalajvm/sttp/tapir/docs/openapi/EndpointToOpenAPIDocsTest.scala | Tests failure/suffixing behavior for duplicate reusable component names. |
| docs/openapi-docs/src/test/resources/reusableComponents/expected_marked_response_header.yml | Expected OpenAPI output for reusable response headers. |
| docs/openapi-docs/src/test/resources/reusableComponents/expected_marked_request_header.yml | Expected OpenAPI output for reusable request headers (as parameters). |
| docs/openapi-docs/src/test/resources/reusableComponents/expected_marked_query_two_endpoints.yml | Expected OpenAPI output for reusable query parameters across endpoints. |
| docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/ReusableComponents.scala | Adds reusable component collection + unique naming assignment logic. |
| docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/ReusableComponentAttribute.scala | Adds .reusableComponent attribute marker for endpoint atoms. |
| docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/OpenAPIDocsOptions.scala | Adds failOnDuplicateComponentName option controlling duplicate component naming behavior. |
| docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToParameters.scala | Extracts shared endpoint-input-to-Parameter conversion (used by paths + reusable component discovery). |
| docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOperationResponse.scala | Updates header generation to reference reusable response headers. |
| docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIPaths.scala | Updates operation parameter generation to reference reusable components and reuses shared conversion. |
| docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIDocs.scala | Wires reusable components pre-pass into OpenAPI generation pipeline. |
| docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToOpenAPIComponents.scala | Emits collected reusable parameters/headers into components. |
| docs/openapi-docs/src/main/scala/sttp/tapir/docs/openapi/EndpointToHeaders.scala | Adds shared output-header-to-Header conversion (used by responses + reusable component discovery). |
| docs/asyncapi-docs/src/main/scala/sttp/tapir/docs/asyncapi/MessagesForEndpoints.scala | Updates calculateUniqueIds call to renamed parameter. |
| docs/apispec-docs/src/test/scala/sttp/tapir/docs/apispec/schema/CalculateUniqueIdsTest.scala | Adds tests for suffixing, default duplicate-name error, and custom error message. |
| docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/schema.scala | Generalizes calculateUniqueIds to accept a custom duplicate-name error message. |
| doc/docs/openapi.md | Documents failOnDuplicateComponentName and the .reusableComponent feature with examples. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+15
to
22
| failOnDuplicateSchemaName: Boolean = false, | ||
| // Stricter than failOnDuplicateSchemaName on purpose: marking a component is explicit, and its key defaults to the parameter's or | ||
| // header's own name, so a collision means two structurally different things claiming one name - almost always a mistake, and one | ||
| // only the author can resolve. Nothing can break, either: no existing code marks a component. | ||
| failOnDuplicateComponentName: Boolean = true | ||
| ) | ||
|
|
||
| object OpenAPIDocsOptions { |
Comment on lines
+82
to
+87
| // A value may be marked at several use sites. Collapse them, taking the first explicit name, so the result does not depend on how | ||
| // many endpoints happen to reuse it. Sort afterwards: `groupBy` yields an unordered Map, and id suffixing is order-dependent. | ||
| val distinctMarked: Vector[(T, Option[String])] = marked | ||
| .groupBy(_._1) | ||
| .toVector | ||
| .map { case (t, markers) => t -> markers.flatMap(_._2).headOption } |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
magdzikk
commented
Aug 18, 2026
| } | ||
| } | ||
|
|
||
| private def operationParameters(inputs: Vector[EndpointInput.Basic[_]]) = { |
Author
There was a problem hiding this comment.
Moved to EndpointToParameters, so that it can also be used by ReusableComponentsForEndpoints
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1411 by adding support for reusable parameters and headers.
Previously, a parameter shared across many endpoints was serialised in full into every operation, generating a lot of repetition.
How should we mark a parameter as reusable?
Option 1 - considered but rejected
For schemas, we consider it reusable if it has a name. But for parameters, they always have a name, so this approach wouldn't work.
Option 2 - considered but rejected
We could add a config opt-in flag to automatically detect duplicated parameters.
Rejected because:
Option 3, applied here - explicit marker
This PR adds an explicit marker:
Every operation using it then emits
$ref: '#/components/parameters/tenantId', with one definition undercomponents.parameters.Advantages of this approach:
DocsExtensionAttribute(the marker is adocs/openapi-docs-ownedAttributeKey, so nocorechange, so no MiMa surfacein: header, and tapir already routes them through the same collector.This PR also adds support for response headers, which are built a bit differently, as a
Headerobject in a separate collector. However we use the same marker for them, so that it's consistent from the user point of view.Remarks:
I'm making
failOnDuplicateComponentNamedefault to true. This is not consistent with existingfailOnDuplicateComponentName, which defaults to false. That's because I assume that if a user explicitly marks two parameters of the same name as reusable, it's not on purpose and they want to be notified early about it. (For schemas it's different, because they're not marked explicitly)The lookup is keyed by the generated
Parametervalue rather than by input identity, becauseEndpointInput.Queryequality includes itsCodecand codec instances aren't reliably equal across definitions. The consequence is that marking any one use site references every structurally identical parameter, marked or not — which is the desired behaviour here (mark the sharedvalonce), and is documented.Tapir's own
openapi-codegenpreviously did not readcomponents.headersat all - fixed separately in openapi-codegen: support components.headers #5476.