Skip to content

openapi-docs: support reusable parameters and response headers in components (#1411) - #5478

Draft
magdzikk wants to merge 11 commits into
masterfrom
worktree-reusable-components
Draft

openapi-docs: support reusable parameters and response headers in components (#1411)#5478
magdzikk wants to merge 11 commits into
masterfrom
worktree-reusable-components

Conversation

@magdzikk

@magdzikk magdzikk commented Aug 18, 2026

Copy link
Copy Markdown

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:

  • it guesses the user intent, and it might guess it wrong (it could deduplicate unrelated parameters, causing confusion)

Option 3, applied here - explicit marker

This PR adds an explicit marker:

import sttp.tapir.docs.openapi.ReusableComponentAttribute._

val tenantId = query[String]("tenantId").description("The tenant").validate(...).reusableComponent

Every operation using it then emits $ref: '#/components/parameters/tenantId', with one definition under components.parameters.

Advantages of this approach:

  • consistency with the pattern in DocsExtensionAttribute (the marker is a docs/openapi-docs-owned AttributeKey, so no core change, so no MiMa surface
  • request headers come along for free: OpenAPI models them as parameters with in: 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 Header object in a separate collector. However we use the same marker for them, so that it's consistent from the user point of view.

Remarks:

  1. I'm making failOnDuplicateComponentName default to true. This is not consistent with existing failOnDuplicateComponentName, 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)

  2. The lookup is keyed by the generated Parameter value rather than by input identity, because EndpointInput.Query equality includes its Codec and 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 shared val once), and is documented.

  3. Tapir's own openapi-codegen previously did not read components.headers at all - fixed separately in openapi-codegen: support components.headers #5476.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 failOnDuplicateComponentName option (default true) and improve de-duplication error messaging via a generalized calculateUniqueIds.
  • 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 }
Comment thread docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/schema.scala Outdated
magdzikk and others added 4 commits August 18, 2026 15:20
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
}
}

private def operationParameters(inputs: Vector[EndpointInput.Basic[_]]) = {

@magdzikk magdzikk Aug 18, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved to EndpointToParameters, so that it can also be used by ReusableComponentsForEndpoints

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support reusable parameters in components

2 participants