armeria icon indicating copy to clipboard operation
armeria copied to clipboard

feat(docservice): Support Jackson polymorphism annotations

Open YoungHoney opened this issue 4 months ago • 8 comments

Motivation

This pull request implements support for Jackson's polymorphism annotations (@JsonTypeInfo, @JsonSubTypes) in DocService, as requested in the community (issue #6313). Currently, DocService does not correctly generate documentation for annotated services that use inheritance in their DTOs, leading to incomplete specifications. This change adds a new DescriptiveTypeInfoProvider to resolve these polymorphic types and generate accurate JSON Schemas.

However, this feature has uncovered significant and complex build stability issues when running a full parallel build (./gradlew clean build --parallel). This PR serves as both the implementation of the feature and a concrete test case for discussing the build instability it triggers.

Modifications

  • Added JacksonPolymorphismTypeInfoProvider: A new provider that uses pure Java reflection to safely inspect @JsonTypeInfo and @JsonSubTypes annotations. It is registered via Java's SPI mechanism to be discoverable by DocService.
  • Added DiscriminatorInfo: A new data class to hold polymorphism metadata extracted from the annotations.
  • Consolidated Type Utilities: General-purpose type conversion logic (e.g., toTypeSignature) was moved from a separate DocServiceTypeUtil into AnnotatedDocServicePlugin for better cohesion.
  • Updated StructInfo: Modified to include oneOf and discriminator fields to carry polymorphism information.
  • Updated JsonSchemaGenerator: The generator now recognizes the new fields in StructInfo and correctly produces JSON Schema with oneOf and discriminator properties.
  • Added PolymorphismDocServiceExample: A new example service to demonstrate and manually verify the feature.

Result

  • DocService can now correctly generate documentation for annotated services that use polymorphic types with Jackson. The resulting JSON Schema will contain the appropriate oneOf and discriminator fields.
  • ~~Known Issue: This change is known to trigger build instability in the project's CI environment. A detailed summary of the investigation is provided here : #6369~~

Example usage

also, you can try this at PolymorphismDocServiceExample

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "species")
@JsonSubTypes({
    @JsonSubTypes.Type(value = Dog.class, name = "dog"),
    @JsonSubTypes.Type(value = Cat.class, name = "cat")
})
interface Animal {
    // ...
}

"structs" : [ {
    "name" : "example.armeria.server.animal.PolymorphismDocServiceExample$Animal",
    "fields" : [ ],
    "descriptionInfo" : {
      "docString" : "",
      "markup" : "NONE"
    },
    "oneOf" : [ "example.armeria.server.animal.PolymorphismDocServiceExample$Dog", "example.armeria.server.animal.PolymorphismDocServiceExample$Cat" ],
    "discriminator" : {
      "propertyName" : "species",
      "mapping" : {
        "dog" : "#/definitions/example.armeria.server.animal.PolymorphismDocServiceExample$Dog",
        "cat" : "#/definitions/example.armeria.server.animal.PolymorphismDocServiceExample$Cat"
      }
    }

YoungHoney avatar Aug 26 '25 08:08 YoungHoney

CLA assistant check
All committers have signed the CLA.

CLAassistant avatar Aug 26 '25 08:08 CLAassistant

However, this feature has uncovered significant and complex build stability issues when running a full parallel build (./gradlew clean build --parallel).

I will investigate it. It seems like your changes aren't related to the failure, so please feel free to change the draft status when you are ready.

minwoox avatar Aug 28 '25 00:08 minwoox

Codecov Report

:x: Patch coverage is 82.48588% with 62 lines in your changes missing coverage. Please review. :white_check_mark: Project coverage is 74.11%. Comparing base (8150425) to head (4823d7b). :warning: Report is 182 commits behind head on main.

Files with missing lines Patch % Lines
...ecorp/armeria/server/docs/JsonSchemaGenerator.java 88.38% 11 Missing and 12 partials :warning:
...a/com/linecorp/armeria/server/docs/StructInfo.java 42.30% 12 Missing and 3 partials :warning:
...inecorp/armeria/server/docs/DiscriminatorInfo.java 43.75% 9 Missing :warning:
...meria/internal/server/docs/DocServiceTypeUtil.java 89.47% 3 Missing and 5 partials :warning:
...rver/docs/JacksonPolymorphismTypeInfoProvider.java 81.57% 3 Missing and 4 partials :warning:
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #6370      +/-   ##
============================================
- Coverage     74.46%   74.11%   -0.35%     
- Complexity    22234    23016     +782     
============================================
  Files          1963     2063     +100     
  Lines         82437    86197    +3760     
  Branches      10764    11334     +570     
============================================
+ Hits          61385    63889    +2504     
- Misses        15918    16888     +970     
- Partials       5134     5420     +286     

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

:rocket: New features to boost your workflow:
  • :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • :package: JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

codecov[bot] avatar Sep 03 '25 01:09 codecov[bot]

🔍 Build Scan® (commit: 57d4302edaf039a924fd3c2c4e31ad6728fd6b16)

Job name Status Build Scan®
build-ubicloud-standard-16-jdk-8 https://ge.armeria.dev/s/cfsotxxiuclni
build-ubicloud-standard-16-jdk-25 https://ge.armeria.dev/s/67umwifxcvx7c
build-ubicloud-standard-16-jdk-21-snapshot-blockhound https://ge.armeria.dev/s/bsp2mpjuv4rgu
build-ubicloud-standard-16-jdk-17-min-java-17-coverage ❌ (failure) https://ge.armeria.dev/s/v2ztmqjwh5y3s
build-ubicloud-standard-16-jdk-17-min-java-11 ❌ (failure) https://ge.armeria.dev/s/m3tj3hvz2md6g
build-ubicloud-standard-16-jdk-17-leak https://ge.armeria.dev/s/7pm65nii5xmte
build-ubicloud-standard-16-jdk-11 https://ge.armeria.dev/s/dxfvdk3bf34aq
build-macos-latest-jdk-25 https://ge.armeria.dev/s/bkdazrw3gvh5a

github-actions[bot] avatar Sep 03 '25 02:09 github-actions[bot]

I found a couple of issues in the generated JSON schema:

  • There are many duplicate definitions because each method has its own ID and definitions.
    [
      {
        "$id": "...",
        "definitions": { ... } // duplicate definitions
      },
      {
        "$id": "...",
        "definitions": { ... } // duplicate definitions
      }
    ]
    
  • The "Cat" and "Dog" definitions are missing the species property, which is causing Autocomplete to fail.

To address this, I propose the following:

  • Use a root object with "$defs/methods" and "$defs/models" to put all methods and structs a single time.
    • It's worth noting that "definitions" is deprecated, as mentioned in the JSON Schema draft specification. https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-00#rfc.appendix.G
    {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "$id": "...",
      "title": "...",
    
      "$defs": {
        "methods": {
          "processAnimal": {
            "$id": "com.linecorp.armeria.server.docs.PolymorphismDocServiceTest$AnimalService/processAnimal/POST",
            "title": "processAnimal",
            "type": "object",
            "properties": {
              "animal": {
                "$ref": "#/$defs/models/Animal" 
              }
            },
            "required": [ "animal" ]
          },
          "processZoo": {
            ...
          }
        },
    
        "models": {
          "Animal": {
            "type": "object",
            "oneOf": [
              { "$ref": "#/$defs/models/Dog" },
              { "$ref": "#/$defs/models/Cat" }
            ],
            "discriminator": {
              "propertyName": "species",
              "mapping": {
                "dog": "#/$defs/models/Dog",
                "cat": "#/$defs/models/Cat"
              }
            }
          },
          "Cat": {
            "type": "object",
            "properties": {
              "species": { "type": "string" },
              "name": { "type": "string" },
              "likesTuna": { "type": "boolean" },
              "scratchPost": { "$ref": "#/$defs/models/Toy" },
              "vetRecord": { "$ref": "#/$defs/models/VetRecord" }
            },
            "required": [ "name", "likesTuna", "scratchPost", "vetRecord" ]
          },
          "Dog": {
            ...
          },
          ...
        }
      }
    }
    
  • Update RequestBody.tsx to align with the new schema format. (I might help you if you are not familiar with the frontend)

Please, let me know your opinion. 🙇

minwoox avatar Sep 19 '25 07:09 minwoox

Use a root object with "$defs/methods" and "$defs/models" to put all methods and structs a single time.

* It's worth noting that "definitions" is deprecated, as mentioned in the JSON Schema draft specification

Thank you for your Review !

I've changed JsonSchemaGenerator based on your feedback, and I agree that the new structure is better than duplicated definitions . Here is the new output :

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "com.....$AnimalService",
  "title": "com....$AnimalService",
  "$defs": {
    "models": {
      "com....$Animal": {
        "type": "object",
        "title": "com....$Animal",
        "oneOf": [
          {
            "$ref": "#/$defs/models/com...$Dog"
          },
          {
            "$ref": "#/$defs/models/com.l...$Cat"
          }
        ],
        "discriminator": {
          "propertyName": "species",
          "mapping": {
            "dog": "#/$defs/models/com....$Dog",
            "cat": "#/$defs/models/com.....$Cat"
          }
        }
      },
      "com....$Cat": {
        "type": "object",
        "title": "com....$Cat",
        "properties": {
          "species": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "likesTuna": {
            "type": "boolean"
          },
          "scratchPost": {
            "$ref": "#/$defs/models/com....$Toy"
          },
          "vetRecord": {
            "$ref": "#/$defs/models/com....$VetRecord"
          }
        },
        "required": [ "name", "likesTuna", "scratchPost", "vetRecord", "species" ] // Should "species" be first? 
      },
      "...": "..."
    }, //models
    "methods": {
      "processAnimal": {
        "$id": "com....$AnimalService/processAnimal/POST",
        "title": "processAnimal",
        "additionalProperties": false,
        "type": "object",
        "properties": {
          "animal": {
            "$ref": "#/$defs/models/com....$Animal"
          }
        },
        "required": [ "animal" ]
      },
      "...": "..."
    } //methods
  }
}

However, this change caused the existing GrpcDocServiceJsonSchemaTest to fail. This brings me to my main question:

Should I maintain backward compatibility for the gRPC schema, or should I update it to use the new, unified structure as well?

Also, as you mentioned, I'm not familiar with the frontend, so I would really appreciate your help with the RequestBody.tsx changes when the time comes.

YoungHoney avatar Sep 22 '25 15:09 YoungHoney

Should I maintain backward compatibility for the gRPC schema, or should I update it to use the new, unified structure as well?

I think we don't have to worry about the compatibility because the browser will fetch the new JSON schema and use it for the autocompletion.

Also, as you mentioned, I'm not familiar with the frontend, so I would really appreciate your help with the RequestBody.tsx changes when the time comes.

I'm happy to help you. 😉 Will push a commit after the server-side changes are done.

minwoox avatar Sep 23 '25 05:09 minwoox

Walkthrough

This PR refactors type signature utilities into a centralized location, introduces polymorphic type support in JSON schemas with discriminator metadata, and modernizes JSON schema generation to use a modular structure with definitions-in-definitions organization.

Changes

Cohort / File(s) Summary
Type Utility Centralization
core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServicePlugin.java, core/src/main/java/com/linecorp/armeria/internal/server/docs/DocServiceTypeUtil.java
Extracts type-to-TypeSignature conversion logic from AnnotatedDocServicePlugin into new DocServiceTypeUtil utility with predefined constants (VOID, BOOLEAN, INT, LONG, STRING, etc.) and two overloaded toTypeSignature methods handling Java types, Jackson types, collections, maps, and optionals.
Import Migration
core/src/main/java/com/linecorp/armeria/internal/server/annotation/DefaultDescriptiveTypeInfoProvider.java, core/src/main/java/com/linecorp/armeria/internal/server/annotation/ReflectiveDescriptiveTypeInfoProvider.java, core/src/test/java/com/linecorp/armeria/internal/server/annotation/*.java, kotlin/src/test/kotlin/..., scala/scala_2.13/src/test/scala/...
Updates static imports across test and source files to reference toTypeSignature and type constants from DocServiceTypeUtil instead of AnnotatedDocServicePlugin.
Polymorphism Infrastructure
core/src/main/java/com/linecorp/armeria/server/docs/DiscriminatorInfo.java, core/src/main/java/com/linecorp/armeria/internal/server/docs/JacksonPolymorphismTypeInfoProvider.java, core/src/main/resources/META-INF/services/com.linecorp.armeria.server.docs.DescriptiveTypeInfoProvider
Introduces DiscriminatorInfo class for OpenAPI discriminator metadata and JacksonPolymorphismTypeInfoProvider implementation that discovers polymorphic types via Jackson annotations, registering the provider via ServiceLoader.
StructInfo Enhancements
core/src/main/java/com/linecorp/armeria/server/docs/StructInfo.java
Adds oneOf (List<TypeSignature>) and discriminator (DiscriminatorInfo) fields to support polymorphic types; updates constructors, builders, equality, hashing, and toString accordingly.
JSON Schema Generation Refactoring
core/src/main/java/com/linecorp/armeria/server/docs/JsonSchemaGenerator.java, core/src/main/java/com/linecorp/armeria/server/docs/DocService.java
Restructures schema generation from linear ArrayNode flow to modular ObjectNode with $defs/models and $defs/methods organization; adds generateModels, generateMethods, generateFieldSchema, and getSchemaType methods; supports discriminator injection and polymorphism mapping; updates return type from ArrayNode to ObjectNode.
Test Infrastructure & Coverage
core/src/test/java/com/linecorp/armeria/internal/server/annotation/DocServiceTestUtil.java, core/src/test/java/com/linecorp/armeria/internal/server/annotation/PolymorphismDocServiceTest.java, core/src/test/java/com/linecorp/armeria/server/docs/JsonSchemaGeneratorTest.java, grpc/src/test/java/com/linecorp/armeria/internal/server/grpc/GrpcDocServiceJsonSchemaTest.java
Adds DocServiceTestUtil for test access to DefaultDescriptiveTypeInfoProvider; introduces comprehensive PolymorphismDocServiceTest with animal DTOs and polymorphic endpoint coverage; refactors JsonSchemaGeneratorTest with helper methods and parameterized specs; updates GrpcDocServiceJsonSchemaTest to use single-schema ObjectNode structure with reference helpers.

Sequence Diagram

sequenceDiagram
    participant User as Client
    participant DocService
    participant JsonSchemaGenerator
    participant JacksonPolymorphismTypeInfoProvider
    participant DiscriminatorInfo
    participant StructInfo

    User->>DocService: Request service specification
    DocService->>JsonSchemaGenerator: generate(serviceSpecification)
    JsonSchemaGenerator->>JsonSchemaGenerator: Inspect types for polymorphism
    JsonSchemaGenerator->>JacksonPolymorphismTypeInfoProvider: Discover polymorphic base types
    JacksonPolymorphismTypeInfoProvider->>JacksonPolymorphismTypeInfoProvider: Check `@JsonTypeInfo` & `@JsonSubTypes`
    JacksonPolymorphismTypeInfoProvider->>DiscriminatorInfo: Create discriminator mapping
    DiscriminatorInfo-->>JacksonPolymorphismTypeInfoProvider: propertyName + subtype mapping
    JacksonPolymorphismTypeInfoProvider->>StructInfo: Build StructInfo with oneOf & discriminator
    StructInfo-->>JacksonPolymorphismTypeInfoProvider: StructInfo ready
    JsonSchemaGenerator->>JsonSchemaGenerator: generateModels() & generateMethods()
    JsonSchemaGenerator->>JsonSchemaGenerator: Inject discriminator into polymorphic struct schemas
    JsonSchemaGenerator->>JsonSchemaGenerator: Build $defs with models and methods
    JsonSchemaGenerator-->>DocService: ObjectNode with unified schema
    DocService-->>User: Complete service documentation

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • JsonSchemaGenerator.java: Substantial refactoring of core schema generation pipeline with new modular structure, discriminator wiring, and polymorphism mapping logic
  • JacksonPolymorphismTypeInfoProvider.java: New polymorphic type discovery and StructInfo construction logic; requires understanding of Jackson annotation introspection
  • StructInfo.java: Addition of polymorphism fields affects constructors, builders, serialization, and equality—verify all propagation paths are correct
  • Test coverage validation: Ensure PolymorphismDocServiceTest and updated JsonSchemaGeneratorTest adequately exercise new polymorphism and schema structure changes

Suggested labels

new feature

Suggested reviewers

  • trustin
  • minwoox
  • ikhoon

Poem

🐰 A rabbit hops through types so grand, Polymorphism now hand in hand! Discriminators map the way, Schemas unified to stay. DocService blooms—what a day! 🌸

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.66% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(docservice): Support Jackson polymorphism annotations' is directly related to the main change: adding support for Jackson's @JsonTypeInfo and @JsonSubTypes annotations in DocService.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the motivation, modifications, and results of implementing Jackson polymorphism support in DocService.
✨ Finishing touches
  • [ ] 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • [ ] Create PR with unit tests
  • [ ] Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

coderabbitai[bot] avatar Dec 03 '25 13:12 coderabbitai[bot]