-
Notifications
You must be signed in to change notification settings - Fork 967
feat(docservice): Support Jackson polymorphism annotations #6370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
I will investigate it. |
Adds a new `JacksonPolymorphismTypeInfoProvider` to generate correct JSON Schemas with `oneOf` and `discriminator` for polymorphic types annotated with `@JsonTypeInfo` and `@JsonSubTypes`.
4e4ea3c
to
ac2b9e3
Compare
core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServiceTest.java
Outdated
Show resolved
Hide resolved
core/src/test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServiceTest.java
Outdated
Show resolved
Hide resolved
...test/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServicePluginTest.java
Outdated
Show resolved
Hide resolved
...ain/resources/META-INF/services/com.linecorp.armeria.server.docs.DescriptiveTypeInfoProvider
Outdated
Show resolved
Hide resolved
Adds a new `JacksonPolymorphismTypeInfoProvider` to generate correct JSON Schemas with `oneOf` and `discriminator` for polymorphic types annotated with `@JsonTypeInfo` and `@JsonSubTypes`.
ac2b9e3
to
e7a0237
Compare
Codecov Report❌ Patch coverage is 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 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
logger.info("JSON Specification: http://127.0.0.1:8080/docs/specification.json"); | ||
} | ||
|
||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "species") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like the subtypes are missing species
, so deserialization actually doesn't work.
Also, this is not a tutorial, but just an example.
So, what do you think of making this class a test case (PolymorphismDocServiceTest) like we did for AnnotatedDocServiceTest
?
We can use TestUtil.isDocServiceDemoMode()
to see how it works on a browser.
Could you also use English for comments instead of Korean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the detailed and helpful feedback! I've addressed all of your suggestions based on our discussion.
-
Converted Example to a Test Case: As you suggested, the
PolymorphismDocServiceExample.java
has been removed entirely. -
Created a Comprehensive Test Suite: I've added a new integration test,
PolymorphismDocServiceTest.java
. This new test suite now covers:- Correct documentation generation for polymorphic types (
oneOf
,discriminator
). - deserialization of various polymorphic objects (
Dog
,Cat
). - handling of edge cases, including misconfigured
@JsonSubTypes({})
annotations and other types likeOptional
andMap
. - I also confirmed that the
TestUtil.isDocServiceDemoMode()
works correctly for manual UI verification.
- Correct documentation generation for polymorphic types (
-
Used English for Comments: I've reviewed all new and modified files to ensure all comments are in English now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a few more suggestions. 😎
Please, keep up the great work. 👍
core/src/main/java/com/linecorp/armeria/server/docs/DiscriminatorInfo.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/server/docs/DocServiceTypeUtil.java
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/server/docs/JacksonPolymorphismTypeInfoProvider.java
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/server/docs/JacksonPolymorphismTypeInfoProvider.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/server/docs/JacksonPolymorphismTypeInfoProvider.java
Show resolved
Hide resolved
if (structInfo != null) { | ||
visited.put(firstParam.typeSignature(), "#"); | ||
generateProperties(structInfo.fields(), visited, "#", root); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we warn if structInfo == null
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the code to handle the structInfo == null
case as you suggested.
if (structInfo != null) {
visited.put(firstParam.typeSignature(), "#");
generateProperties(structInfo.fields(), visited, "#", root);
} else {
logger.warn("Could not find root struct for signature: {}",
firstParam.typeSignature().signature());
root.put("additionalProperties", true);
}
Now, if a StructInfo
for a gRPC request(may be) is not found:
- A warning will be logged with the missing
TypeSignature
. - The generated schema for that method will fall back to using
"additionalProperties": true
, effectively treating it as a generic object.
core/src/main/java/com/linecorp/armeria/server/docs/JsonSchemaGenerator.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/server/docs/JsonSchemaGenerator.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/linecorp/armeria/server/docs/JsonSchemaGenerator.java
Outdated
Show resolved
Hide resolved
fieldNode.set("additionalProperties", additionalPropertiesNode.get("")); | ||
} | ||
break; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happends if the type is other types such as CONTAINER
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've analyzed this and improved the generator's logic to handle these cases more robustly by “unwrapping” them to their inner types.
Implemented Solution
I've updated both getSchemaType
and generateFieldSchema
in JsonSchemaGenerator
to recursively process OPTIONAL
and CONTAINER
types.
Code Snippet from JsonSchemaGenerator.java
:
// In getSchemaType()
case OPTIONAL:
case CONTAINER: {
// Unwrap and return the inner type's schema type
final TypeSignature inner =
((ContainerTypeSignature) typeSignature).typeParameters().get(0);
return getSchemaType(inner);
}
// In generateFieldSchema()
if (typeSignature.type() == TypeSignatureType.OPTIONAL ||
typeSignature.type() == TypeSignatureType.CONTAINER) {
final TypeSignature inner =
((ContainerTypeSignature) typeSignature).typeParameters().get(0);
final ObjectNode innerNode = generateFieldSchema(FieldInfo.of("", inner));
fieldNode.setAll(innerNode);
return fieldNode;
}
Example and Results
To verify this, I've used the following service methods in the test code PolymorphismDocServiceTest
:
// Container example
static final class ApiResponse<T> {
@JsonProperty
private final int status;
@JsonProperty
private final T data;
ApiResponse(int status, T data) {
this.status = status;
this.data = data;
}
}
// OPTIONAL type in parameter
@Post("/animal/optional")
public String processOptionalAnimal(Optional<Animal> animal) { ... }
// CONTAINER type in return value
@Post("/dummy/api_response")
public ApiResponse<Toy> getExampleResponse() { ... }
1. Result in specification.json
:
The DocService
correctly unwraps Optional<Animal>
to its inner type Animal
and marks it as OPTIONAL
. For ApiResponse<Toy>
, it correctly identifies the full generic type signature.
// For processOptionalAnimal
{
"id" : "...AnimalService/processOptionalAnimal/POST",
"name" : "processOptionalAnimal",
"returnTypeSignature" : "string",
"parameters" : [ {
"name" : "animal",
"location" : "UNSPECIFIED",
"requirement" : "OPTIONAL",
"typeSignature" : "...$Animal",
"descriptionInfo" : {
"docString" : "",
"markup" : "NONE"
}
}
// For getExampleResponse
{
...
"returnTypeSignature": "ApiResponse<...PolymorphismDocServiceTest$Toy>"
...
}
2. Result in schemas.json
:
// part of Schema for processOptionalAnimal
{
"$id": ".../processOptionalAnimal/POST",
"properties": {
"animal": {
"$ref": "#/definitions/...PolymorphismDocServiceTest$Animal"
}
}
...
}
//part of Schema for getExampleResponse
{
"$id" : ".../getExampleResponse/POST",
"title" : "getExampleResponse",
"additionalProperties" : false,
"type" : "object",
...
}
Adds a new `JacksonPolymorphismTypeInfoProvider` to generate correct JSON Schemas with `oneOf` and `discriminator` for polymorphic types annotated with `@JsonTypeInfo` and `@JsonSubTypes`.
e7a0237
to
4823d7b
Compare
I found a couple of issues in the generated JSON schema:
To address this, I propose the following:
Please, let me know your opinion. 🙇 |
Thank you for your Review ! I've changed {
"$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 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 |
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.
I'm happy to help you. 😉 Will push a commit after the server-side changes are done. |
4823d7b
to
e02df82
Compare
e02df82
to
6017c7d
Compare
Motivation
This pull request implements support for Jackson's polymorphism annotations (
@JsonTypeInfo
,@JsonSubTypes
) inDocService
, 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 newDescriptiveTypeInfoProvider
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
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 byDocService
.DiscriminatorInfo
: A new data class to hold polymorphism metadata extracted from the annotations.toTypeSignature
) was moved from a separateDocServiceTypeUtil
intoAnnotatedDocServicePlugin
for better cohesion.StructInfo
: Modified to includeoneOf
anddiscriminator
fields to carry polymorphism information.JsonSchemaGenerator
: The generator now recognizes the new fields inStructInfo
and correctly produces JSON Schema withoneOf
anddiscriminator
properties.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 appropriateoneOf
anddiscriminator
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 : Request Guidance on Build Issues in my feature branch #6369Example usage
also, you can try this at PolymorphismDocServiceExample