Skip to content

Commit e7a0237

Browse files
committed
feat(docservice): Support Jackson polymorphism annotations(line#6370)
Adds a new `JacksonPolymorphismTypeInfoProvider` to generate correct JSON Schemas with `oneOf` and `discriminator` for polymorphic types annotated with `@JsonTypeInfo` and `@JsonSubTypes`.
1 parent c0bef4c commit e7a0237

17 files changed

Lines changed: 1183 additions & 581 deletions

File tree

core/src/main/java/com/linecorp/armeria/internal/server/annotation/AnnotatedDocServicePlugin.java

Lines changed: 1 addition & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,28 @@
1616

1717
package com.linecorp.armeria.internal.server.annotation;
1818

19-
import static com.google.common.collect.ImmutableList.toImmutableList;
2019
import static com.google.common.collect.ImmutableSet.toImmutableSet;
2120
import static com.linecorp.armeria.internal.server.annotation.KotlinUtil.isKFunction;
2221
import static com.linecorp.armeria.internal.server.annotation.KotlinUtil.isReturnTypeNothing;
2322
import static com.linecorp.armeria.internal.server.annotation.KotlinUtil.kFunctionGenericReturnType;
2423
import static com.linecorp.armeria.internal.server.annotation.KotlinUtil.kFunctionReturnType;
24+
import static com.linecorp.armeria.server.docs.DocServiceTypeUtil.toTypeSignature;
2525
import static com.linecorp.armeria.server.docs.FieldLocation.HEADER;
2626
import static com.linecorp.armeria.server.docs.FieldLocation.PATH;
2727
import static com.linecorp.armeria.server.docs.FieldLocation.QUERY;
2828
import static com.linecorp.armeria.server.docs.FieldLocation.UNSPECIFIED;
2929
import static java.util.Objects.requireNonNull;
3030

3131
import java.lang.annotation.Annotation;
32-
import java.lang.reflect.GenericArrayType;
3332
import java.lang.reflect.Method;
34-
import java.lang.reflect.ParameterizedType;
35-
import java.lang.reflect.Type;
36-
import java.lang.reflect.TypeVariable;
37-
import java.lang.reflect.WildcardType;
38-
import java.nio.ByteBuffer;
3933
import java.util.HashMap;
4034
import java.util.HashSet;
4135
import java.util.List;
4236
import java.util.Map;
43-
import java.util.Optional;
4437
import java.util.Set;
45-
import java.util.stream.Stream;
4638

4739
import com.fasterxml.jackson.core.JsonProcessingException;
4840
import com.fasterxml.jackson.core.TreeNode;
49-
import com.fasterxml.jackson.databind.JavaType;
5041
import com.fasterxml.jackson.databind.ObjectWriter;
5142
import com.google.common.annotations.VisibleForTesting;
5243
import com.google.common.collect.ImmutableList;
@@ -84,36 +75,11 @@
8475
import com.linecorp.armeria.server.docs.TypeSignature;
8576
import com.linecorp.armeria.server.docs.TypeSignatureType;
8677

87-
import io.netty.buffer.ByteBuf;
88-
8978
/**
9079
* A {@link DocServicePlugin} implementation that supports the {@link AnnotatedService}.
9180
*/
9281
public final class AnnotatedDocServicePlugin implements DocServicePlugin {
9382

94-
@VisibleForTesting
95-
static final TypeSignature VOID = TypeSignature.ofBase("void");
96-
@VisibleForTesting
97-
static final TypeSignature BOOLEAN = TypeSignature.ofBase("boolean");
98-
@VisibleForTesting
99-
static final TypeSignature BYTE = TypeSignature.ofBase("byte");
100-
@VisibleForTesting
101-
static final TypeSignature SHORT = TypeSignature.ofBase("short");
102-
@VisibleForTesting
103-
static final TypeSignature INT = TypeSignature.ofBase("int");
104-
@VisibleForTesting
105-
static final TypeSignature LONG = TypeSignature.ofBase("long");
106-
@VisibleForTesting
107-
static final TypeSignature FLOAT = TypeSignature.ofBase("float");
108-
@VisibleForTesting
109-
static final TypeSignature DOUBLE = TypeSignature.ofBase("double");
110-
@VisibleForTesting
111-
static final TypeSignature CHAR = TypeSignature.ofBase("char");
112-
@VisibleForTesting
113-
static final TypeSignature STRING = TypeSignature.ofBase("string");
114-
@VisibleForTesting
115-
static final TypeSignature BINARY = TypeSignature.ofBase("binary");
116-
11783
private static final ObjectWriter objectWriter = JacksonUtil.newDefaultObjectMapper()
11884
.writerWithDefaultPrettyPrinter();
11985

@@ -311,121 +277,6 @@ private static FieldInfo fieldInfo(AnnotatedValueResolver resolver) {
311277
.build();
312278
}
313279

314-
static TypeSignature toTypeSignature(Type type) {
315-
requireNonNull(type, "type");
316-
317-
if (type instanceof JavaType) {
318-
return toTypeSignature((JavaType) type);
319-
}
320-
321-
// The data types defined by the OpenAPI Specification:
322-
323-
if (type == Void.class || type == void.class) {
324-
return VOID;
325-
}
326-
if (type == Boolean.class || type == boolean.class) {
327-
return BOOLEAN;
328-
}
329-
if (type == Byte.class || type == byte.class) {
330-
return BYTE;
331-
}
332-
if (type == Short.class || type == short.class) {
333-
return SHORT;
334-
}
335-
if (type == Integer.class || type == int.class) {
336-
return INT;
337-
}
338-
if (type == Long.class || type == long.class) {
339-
return LONG;
340-
}
341-
if (type == Float.class || type == float.class) {
342-
return FLOAT;
343-
}
344-
if (type == Double.class || type == double.class) {
345-
return DOUBLE;
346-
}
347-
if (type == Character.class || type == char.class) {
348-
return CHAR;
349-
}
350-
if (type == String.class) {
351-
return STRING;
352-
}
353-
if (type == byte[].class || type == Byte[].class ||
354-
type == ByteBuffer.class || type == ByteBuf.class) {
355-
return BINARY;
356-
}
357-
// End of data types defined by the OpenAPI Specification.
358-
359-
if (type instanceof ParameterizedType) {
360-
final ParameterizedType parameterizedType = (ParameterizedType) type;
361-
final Class<?> rawType = (Class<?>) parameterizedType.getRawType();
362-
if (List.class.isAssignableFrom(rawType)) {
363-
return TypeSignature.ofList(toTypeSignature(parameterizedType.getActualTypeArguments()[0]));
364-
}
365-
if (Set.class.isAssignableFrom(rawType)) {
366-
return TypeSignature.ofSet(toTypeSignature(parameterizedType.getActualTypeArguments()[0]));
367-
}
368-
369-
if (Map.class.isAssignableFrom(rawType)) {
370-
final TypeSignature key = toTypeSignature(parameterizedType.getActualTypeArguments()[0]);
371-
final TypeSignature value = toTypeSignature(parameterizedType.getActualTypeArguments()[1]);
372-
return TypeSignature.ofMap(key, value);
373-
}
374-
375-
if (Optional.class.isAssignableFrom(rawType) || "scala.Option".equals(rawType.getName())) {
376-
return TypeSignature.ofOptional(toTypeSignature(parameterizedType.getActualTypeArguments()[0]));
377-
}
378-
379-
final List<TypeSignature> actualTypes = Stream.of(parameterizedType.getActualTypeArguments())
380-
.map(AnnotatedDocServicePlugin::toTypeSignature)
381-
.collect(toImmutableList());
382-
return TypeSignature.ofContainer(rawType.getSimpleName(), actualTypes);
383-
}
384-
385-
if (type instanceof WildcardType) {
386-
// Create an unresolved type with an empty string so that the type name will be '?'.
387-
return TypeSignature.ofUnresolved("");
388-
}
389-
if (type instanceof TypeVariable) {
390-
return TypeSignature.ofBase(type.getTypeName());
391-
}
392-
if (type instanceof GenericArrayType) {
393-
return TypeSignature.ofList(toTypeSignature(((GenericArrayType) type).getGenericComponentType()));
394-
}
395-
396-
if (!(type instanceof Class)) {
397-
return TypeSignature.ofBase(type.getTypeName());
398-
}
399-
400-
final Class<?> clazz = (Class<?>) type;
401-
if (clazz.isArray()) {
402-
// If it's an array, return it as a list.
403-
return TypeSignature.ofList(toTypeSignature(clazz.getComponentType()));
404-
}
405-
406-
return TypeSignature.ofStruct(clazz);
407-
}
408-
409-
static TypeSignature toTypeSignature(JavaType type) {
410-
if (type.isArrayType() || type.isCollectionLikeType()) {
411-
return TypeSignature.ofList(toTypeSignature(type.getContentType()));
412-
}
413-
414-
if (type.isMapLikeType()) {
415-
final TypeSignature key = toTypeSignature(type.getKeyType());
416-
final TypeSignature value = toTypeSignature(type.getContentType());
417-
return TypeSignature.ofMap(key, value);
418-
}
419-
420-
if (Optional.class.isAssignableFrom(type.getRawClass()) ||
421-
"scala.Option".equals(type.getRawClass().getName())) {
422-
return TypeSignature.ofOptional(
423-
toTypeSignature(type.getBindings().getBoundType(0)));
424-
}
425-
426-
return toTypeSignature(type.getRawClass());
427-
}
428-
429280
private static FieldLocation location(AnnotatedValueResolver resolver) {
430281
if (resolver.isPathVariable()) {
431282
return PATH;

core/src/main/java/com/linecorp/armeria/internal/server/annotation/DefaultDescriptiveTypeInfoProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
import static com.google.common.base.MoreObjects.firstNonNull;
2020
import static com.google.common.collect.ImmutableList.toImmutableList;
21-
import static com.linecorp.armeria.internal.server.annotation.AnnotatedDocServicePlugin.toTypeSignature;
2221
import static com.linecorp.armeria.internal.server.annotation.AnnotatedValueResolver.isAnnotatedNullable;
22+
import static com.linecorp.armeria.server.docs.DocServiceTypeUtil.toTypeSignature;
2323
import static java.util.Objects.requireNonNull;
2424

2525
import java.lang.reflect.AnnotatedElement;

core/src/main/java/com/linecorp/armeria/internal/server/annotation/ReflectiveDescriptiveTypeInfoProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
package com.linecorp.armeria.internal.server.annotation;
1818

1919
import static com.google.common.collect.ImmutableList.toImmutableList;
20-
import static com.linecorp.armeria.internal.server.annotation.AnnotatedDocServicePlugin.toTypeSignature;
2120
import static com.linecorp.armeria.internal.server.annotation.DefaultDescriptiveTypeInfoProvider.isNullable;
21+
import static com.linecorp.armeria.server.docs.DocServiceTypeUtil.toTypeSignature;
2222

2323
import java.lang.reflect.AnnotatedElement;
2424
import java.lang.reflect.Field;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2025 LY Corporation
3+
*
4+
* LY Corporation licenses this file to you under the Apache License,
5+
* version 2.0 (the "License"); you may not use this file except in compliance
6+
* with the License. You may obtain a copy of the License at:
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
package com.linecorp.armeria.server.docs;
17+
18+
import static java.util.Objects.requireNonNull;
19+
20+
import java.util.Map;
21+
import java.util.Objects;
22+
23+
import com.fasterxml.jackson.annotation.JsonProperty;
24+
import com.google.common.base.MoreObjects;
25+
import com.google.common.collect.ImmutableMap;
26+
27+
import com.linecorp.armeria.common.annotation.Nullable;
28+
import com.linecorp.armeria.common.annotation.UnstableApi;
29+
30+
/**
31+
* Metadata about a discriminator object, which is used for polymorphism.
32+
* This corresponds to the {@code discriminator} object in the OpenAPI Specification.
33+
* @see <a href="https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/">Inheritance and Polymorphism</a>
34+
*/
35+
@UnstableApi
36+
public final class DiscriminatorInfo {
37+
38+
private final String propertyName;
39+
private final Map<String, String> mapping;
40+
41+
/**
42+
* Creates a new instance.
43+
*
44+
* @param propertyName the name of the property in the payload that will be used to differentiate
45+
* between schemas.
46+
* @param mapping a map of payload values to schema names or references.
47+
*/
48+
public DiscriminatorInfo(String propertyName, Map<String, String> mapping) {
49+
this.propertyName = requireNonNull(propertyName, "propertyName");
50+
this.mapping = ImmutableMap.copyOf(requireNonNull(mapping, "mapping"));
51+
}
52+
53+
/**
54+
* Returns the name of the property that is used to differentiate between schemas.
55+
*/
56+
@JsonProperty
57+
public String propertyName() {
58+
return propertyName;
59+
}
60+
61+
/**
62+
* Returns the map of payload values to schema names.
63+
* The keys are the values that appear in the {@link #propertyName()} field, and the values are
64+
* the schema definitions to use for that value (e.g., {@code "#/definitions/Cat"}).
65+
*/
66+
@JsonProperty
67+
public Map<String, String> mapping() {
68+
return mapping;
69+
}
70+
71+
@Override
72+
public boolean equals(@Nullable Object o) {
73+
if (this == o) {
74+
return true;
75+
}
76+
if (!(o instanceof DiscriminatorInfo)) {
77+
return false;
78+
}
79+
final DiscriminatorInfo that = (DiscriminatorInfo) o;
80+
return propertyName.equals(that.propertyName) && mapping.equals(that.mapping);
81+
}
82+
83+
@Override
84+
public int hashCode() {
85+
return Objects.hash(propertyName, mapping);
86+
}
87+
88+
@Override
89+
public String toString() {
90+
return MoreObjects.toStringHelper(this).add("propertyName", propertyName).add("mapping", mapping)
91+
.toString();
92+
}
93+
}

0 commit comments

Comments
 (0)