Skip to content

Apply per-field annotations on $ref, not on referenced schema (#5187)#5189

Open
adamw wants to merge 1 commit intomasterfrom
fix/deprecated-nested-case-class-field-5187
Open

Apply per-field annotations on $ref, not on referenced schema (#5187)#5189
adamw wants to merge 1 commit intomasterfrom
fix/deprecated-nested-case-class-field-5187

Conversation

@adamw
Copy link
Copy Markdown
Member

@adamw adamw commented Apr 22, 2026

Summary

Fixes #5187. @deprecated (and other per-field annotations like @description) on a product field whose type is a named case class were leaking into the referenced component definition instead of appearing on the $ref — so Swagger UI didn't render the field as deprecated.

Root cause: derivation applies field annotations to the field's typeclass, which for a case class is the canonical Schema. ToKeyedSchemas then stored that field-enriched schema as the canonical form in keyToSchema; because originalSchema == schema at the reference site, ToSchemaReference.map suppressed the annotation on the $ref while it lived on the component. combine() already handled the multi-usage divergent case; this was only broken when the type was referenced once or identically at every usage.

Fix: when field annotations modify a named schema, derivation stashes the canonical copy via a new Schema.OriginalForDocs attribute. ToKeyedSchemas uses that canonical copy when populating keyToSchema, so ToSchemaReference.map sees the real difference and attaches the annotation to the $ref.

Test plan

🤖 Generated with Claude Code

Per-field annotations such as `@deprecated` or `@description` on a product
field whose type is a named case class were leaking into the referenced
component definition (and being suppressed on the `$ref`) when the type
was referenced only once, or identically at each usage. Derivation now
preserves the canonical schema via the `Schema.OriginalForDocs` attribute,
so the documentation interpreters attach the annotation to the `$ref`
rather than the component itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 22, 2026 10:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes an OpenAPI/AsyncAPI documentation generation bug where per-field annotations (e.g. @deprecated, @description) applied to a product field of a named schema were being stored on the referenced component schema instead of the $ref, causing tools like Swagger UI to miss the field-level metadata.

Changes:

  • Add Schema.OriginalForDocs attribute to preserve a canonical (un-enriched) schema when field annotations enrich a named schema during derivation.
  • Update ToKeyedSchemas to store the canonical schema in keyToSchema while still allowing $ref enrichment at usage sites.
  • Add regression coverage: a new OpenAPI YAML verification test + updated generic derivation expectations.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
json/pickler/src/main/scala/sttp/tapir/json/pickler/SchemaDerivation.scala Stashes original schema in an attribute when field annotations enrich a named schema (Scala 3 pickler derivation).
core/src/main/scala-3/sttp/tapir/generic/auto/SchemaMagnoliaDerivation.scala Applies OriginalForDocs in Scala 3 Magnolia derivation for product fields.
core/src/main/scala-2/sttp/tapir/generic/auto/SchemaMagnoliaDerivation.scala Applies OriginalForDocs in Scala 2 Magnolia derivation for product fields.
core/src/main/scala/sttp/tapir/Schema.scala Introduces the Schema.OriginalForDocs attribute type/key.
docs/apispec-docs/src/main/scala/sttp/tapir/docs/apispec/schema/ToKeyedSchemas.scala Uses the canonical schema (when present) when populating keyed component schemas.
docs/openapi-docs/src/test/scalajvm/.../VerifyYamlMultiCustomiseSchemaTest.scala Adds regression test for deprecated nested case class field when referenced type is only used once.
docs/openapi-docs/src/test/resources/.../expected_deprecated_only_field.yml Adds expected YAML output verifying deprecated: true appears on the $ref property.
core/src/test/scala/sttp/tapir/generic/SchemaGenericAutoTest.scala Updates expectations to include the new OriginalForDocs attribute on enriched field schemas.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +10 to +15
// #5187: for a product field whose type is a named case class with per-field annotations (e.g. @deprecated),
// derivation preserves the canonical (un-enriched) form via Schema.OriginalForDocs so that those annotations
// don't leak into the referenced component definition. ToSchemaReference.map still observes the difference
// between the canonical and the field-enriched schema, and attaches the annotations to the $ref.
val storeSchema = schema.attribute(TSchema.OriginalForDocs.Attribute).map(_.schema).getOrElse(schema)
val thisSchema = SchemaKey(schema).map(_ -> storeSchema).toList
Copy link

Copilot AI Apr 22, 2026

Choose a reason for hiding this comment

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

storeSchema uses OriginalForDocs to populate keyToSchema with the un-enriched schema. This avoids leaking per-field metadata into components, but it also drops any per-field changes that aren't re-applied in ToSchemaReference.map (e.g. schema.format, validators/constraints, docsExtensions, or arbitrary @Schema.annotations.customise changes). As TSchemaToASchema doesn't add these for $ref schemas, such per-field annotations will silently disappear from the generated docs. Consider either (a) extending ToSchemaReference.map to propagate the additional supported fields (at least format, and possibly constraints/docsExtensions), or (b) only using OriginalForDocs when the enrichment affects properties that can be represented alongside a $ref (description/default/example/deprecated/title/nullable).

Copilot uses AI. Check for mistakes.
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.

[BUG] @deprecated annotation ignored on nested case class fields in Swagger UI

2 participants