Skip to content

Commit ac2b9e3

Browse files
committed
feat(docservice): Support Jackson polymorphism annotations(#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 ac2b9e3

20 files changed

Lines changed: 1670 additions & 1084 deletions

File tree

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

Lines changed: 106 additions & 267 deletions
Large diffs are not rendered by default.

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

Lines changed: 163 additions & 163 deletions
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;
@@ -74,29 +74,6 @@ public final class DefaultDescriptiveTypeInfoProvider implements DescriptiveType
7474
this.request = request;
7575
}
7676

77-
@Nullable
78-
@Override
79-
public DescriptiveTypeInfo newDescriptiveTypeInfo(Object typeDescriptor) {
80-
requireNonNull(typeDescriptor, "typeDescriptor");
81-
if (!(typeDescriptor instanceof Class)) {
82-
return null;
83-
}
84-
final Class<?> clazz = (Class<?>) typeDescriptor;
85-
if (clazz.isEnum()) {
86-
return newEnumInfo(clazz);
87-
}
88-
89-
if (HttpResponse.class.isAssignableFrom(clazz)) {
90-
return HTTP_RESPONSE_INFO;
91-
}
92-
93-
if (request) {
94-
return requestStructInfo(clazz);
95-
} else {
96-
return responseStructInfo(clazz);
97-
}
98-
}
99-
10077
private static EnumInfo newEnumInfo(Class<?> enumClass) {
10178
final String name = enumClass.getName();
10279

@@ -118,123 +95,6 @@ private static EnumInfo newEnumInfo(Class<?> enumClass) {
11895
return new EnumInfo(name, values, classDescriptionInfo(enumClass));
11996
}
12097

121-
private StructInfo requestStructInfo(Class<?> type) {
122-
final JavaType javaType = mapper.constructType(type);
123-
if (!mapper.canDeserialize(javaType)) {
124-
return newReflectiveStructInfo(type);
125-
}
126-
return new StructInfo(type.getName(), requestFieldInfos(javaType),
127-
classDescriptionInfo(javaType.getRawClass()));
128-
}
129-
130-
private List<FieldInfo> requestFieldInfos(JavaType javaType) {
131-
if (!mapper.canDeserialize(javaType)) {
132-
return ImmutableList.of();
133-
}
134-
135-
final BeanDescription description = mapper.getDeserializationConfig().introspect(javaType);
136-
final List<BeanPropertyDefinition> properties = description.findProperties();
137-
if (properties.isEmpty()) {
138-
return newReflectiveStructInfo(javaType.getRawClass()).fields();
139-
}
140-
141-
return properties.stream()
142-
.map(property -> fieldInfos(javaType,
143-
property.getName(),
144-
property.getInternalName(),
145-
property.getPrimaryType()))
146-
.collect(toImmutableList());
147-
}
148-
149-
private StructInfo responseStructInfo(Class<?> type) {
150-
if (!mapper.canSerialize(type)) {
151-
return newReflectiveStructInfo(type);
152-
}
153-
final JavaType javaType = mapper.constructType(type);
154-
return new StructInfo(type.getName(), responseFieldInfos(javaType), classDescriptionInfo(type));
155-
}
156-
157-
private List<FieldInfo> responseFieldInfos(JavaType javaType) {
158-
if (!mapper.canSerialize(javaType.getRawClass())) {
159-
return ImmutableList.of();
160-
}
161-
162-
final BeanDescription description = mapper.getSerializationConfig().introspect(javaType);
163-
final List<BeanPropertyDefinition> properties = description.findProperties();
164-
if (properties.isEmpty()) {
165-
return newReflectiveStructInfo(javaType.getRawClass()).fields();
166-
}
167-
168-
return properties.stream()
169-
.map(property -> fieldInfos(javaType,
170-
property.getName(),
171-
property.getInternalName(),
172-
property.getPrimaryType()))
173-
.collect(toImmutableList());
174-
}
175-
176-
private FieldInfo fieldInfos(JavaType javaType, String name, String internalName, JavaType fieldType) {
177-
TypeSignature typeSignature = toTypeSignature(fieldType);
178-
final FieldRequirement fieldRequirement;
179-
if (typeSignature.type() == TypeSignatureType.OPTIONAL) {
180-
typeSignature = ((ContainerTypeSignature) typeSignature).typeParameters().get(0);
181-
if (typeSignature.type().hasTypeDescriptor()) {
182-
final Object descriptor =
183-
((DescriptiveTypeSignature) typeSignature).descriptor();
184-
if (descriptor instanceof Class) {
185-
fieldType = mapper.constructType((Class<?>) descriptor);
186-
}
187-
}
188-
fieldRequirement = FieldRequirement.OPTIONAL;
189-
} else {
190-
fieldRequirement = fieldRequirement(javaType, fieldType, internalName);
191-
}
192-
193-
final DescriptionInfo descriptionInfo = fieldDescriptionInfo(javaType, fieldType, internalName);
194-
return FieldInfo.builder(name, typeSignature)
195-
.requirement(fieldRequirement)
196-
.descriptionInfo(descriptionInfo)
197-
.build();
198-
}
199-
200-
private FieldRequirement fieldRequirement(JavaType classType, JavaType fieldType, String fieldName) {
201-
if (fieldType.isPrimitive()) {
202-
return FieldRequirement.REQUIRED;
203-
}
204-
205-
if (KotlinUtil.isData(classType.getRawClass())) {
206-
// Only the parameters in the constructor of data classes correctly provide
207-
// `isMarkedNullable` information.
208-
final FieldRequirement requirement =
209-
extractFromConstructor(classType, fieldType, fieldName, parameter -> {
210-
if (isNullable(parameter)) {
211-
return FieldRequirement.OPTIONAL;
212-
} else {
213-
return FieldRequirement.REQUIRED;
214-
}
215-
});
216-
if (requirement != null) {
217-
return requirement;
218-
}
219-
}
220-
221-
final FieldRequirement requirement =
222-
extractFieldMeta(classType, fieldType, fieldName, element -> {
223-
if (request) {
224-
if (element instanceof Method) {
225-
// Use the first parameter information for the setter method
226-
element = ((Method) element).getParameters()[0];
227-
}
228-
}
229-
if (isNullable(element)) {
230-
return FieldRequirement.OPTIONAL;
231-
}
232-
return FieldRequirement.REQUIRED;
233-
});
234-
235-
return firstNonNull(requirement, FieldRequirement.UNSPECIFIED);
236-
}
237-
23898
static boolean isNullable(AnnotatedElement element) {
23999
return isAnnotatedNullable(element) || KotlinUtil.isMarkedNullable(element);
240100
}
@@ -248,28 +108,6 @@ private static DescriptionInfo classDescriptionInfo(Class<?> clazz) {
248108
}
249109
}
250110

251-
private DescriptionInfo fieldDescriptionInfo(JavaType classType, JavaType fieldType, String fieldName) {
252-
final Description description = extractFieldMeta(classType, fieldType, fieldName, element -> {
253-
return AnnotationUtil.findFirst(element, Description.class);
254-
});
255-
256-
if (description != null) {
257-
return DescriptionInfo.from(description);
258-
} else {
259-
return DescriptionInfo.empty();
260-
}
261-
}
262-
263-
@Nullable
264-
private <T> T extractFieldMeta(JavaType classType, JavaType fieldType, String fieldName,
265-
Function<AnnotatedElement, @Nullable T> extractor) {
266-
if (request) {
267-
return extractRequestFieldMeta(classType, fieldType, fieldName, extractor);
268-
} else {
269-
return extractResponseFieldMeta(classType, fieldType, fieldName, extractor);
270-
}
271-
}
272-
273111
@Nullable
274112
private static <T> T extractRequestFieldMeta(JavaType classType, JavaType fieldType, String fieldName,
275113
Function<AnnotatedElement, @Nullable T> extractor) {
@@ -430,4 +268,166 @@ private static <T> T extractFromGetter(JavaType classType, JavaType fieldType, S
430268
private static StructInfo newReflectiveStructInfo(Class<?> clazz) {
431269
return (StructInfo) ReflectiveDescriptiveTypeInfoProvider.INSTANCE.newDescriptiveTypeInfo(clazz);
432270
}
271+
272+
@Nullable
273+
@Override
274+
public DescriptiveTypeInfo newDescriptiveTypeInfo(Object typeDescriptor) {
275+
requireNonNull(typeDescriptor, "typeDescriptor");
276+
if (!(typeDescriptor instanceof Class)) {
277+
return null;
278+
}
279+
final Class<?> clazz = (Class<?>) typeDescriptor;
280+
if (clazz.isEnum()) {
281+
return newEnumInfo(clazz);
282+
}
283+
284+
if (HttpResponse.class.isAssignableFrom(clazz)) {
285+
return HTTP_RESPONSE_INFO;
286+
}
287+
288+
if (request) {
289+
return requestStructInfo(clazz);
290+
} else {
291+
return responseStructInfo(clazz);
292+
}
293+
}
294+
295+
private StructInfo requestStructInfo(Class<?> type) {
296+
final JavaType javaType = mapper.constructType(type);
297+
if (!mapper.canDeserialize(javaType)) {
298+
return newReflectiveStructInfo(type);
299+
}
300+
return new StructInfo(type.getName(), requestFieldInfos(javaType),
301+
classDescriptionInfo(javaType.getRawClass()));
302+
}
303+
304+
private List<FieldInfo> requestFieldInfos(JavaType javaType) {
305+
if (!mapper.canDeserialize(javaType)) {
306+
return ImmutableList.of();
307+
}
308+
309+
final BeanDescription description = mapper.getDeserializationConfig().introspect(javaType);
310+
final List<BeanPropertyDefinition> properties = description.findProperties();
311+
if (properties.isEmpty()) {
312+
return newReflectiveStructInfo(javaType.getRawClass()).fields();
313+
}
314+
315+
return properties.stream()
316+
.map(property -> fieldInfos(javaType,
317+
property.getName(),
318+
property.getInternalName(),
319+
property.getPrimaryType()))
320+
.collect(toImmutableList());
321+
}
322+
323+
private StructInfo responseStructInfo(Class<?> type) {
324+
if (!mapper.canSerialize(type)) {
325+
return newReflectiveStructInfo(type);
326+
}
327+
final JavaType javaType = mapper.constructType(type);
328+
return new StructInfo(type.getName(), responseFieldInfos(javaType), classDescriptionInfo(type));
329+
}
330+
331+
private List<FieldInfo> responseFieldInfos(JavaType javaType) {
332+
if (!mapper.canSerialize(javaType.getRawClass())) {
333+
return ImmutableList.of();
334+
}
335+
336+
final BeanDescription description = mapper.getSerializationConfig().introspect(javaType);
337+
final List<BeanPropertyDefinition> properties = description.findProperties();
338+
if (properties.isEmpty()) {
339+
return newReflectiveStructInfo(javaType.getRawClass()).fields();
340+
}
341+
342+
return properties.stream()
343+
.map(property -> fieldInfos(javaType,
344+
property.getName(),
345+
property.getInternalName(),
346+
property.getPrimaryType()))
347+
.collect(toImmutableList());
348+
}
349+
350+
private FieldInfo fieldInfos(JavaType javaType, String name, String internalName, JavaType fieldType) {
351+
TypeSignature typeSignature = toTypeSignature(fieldType);
352+
final FieldRequirement fieldRequirement;
353+
if (typeSignature.type() == TypeSignatureType.OPTIONAL) {
354+
typeSignature = ((ContainerTypeSignature) typeSignature).typeParameters().get(0);
355+
if (typeSignature.type().hasTypeDescriptor()) {
356+
final Object descriptor =
357+
((DescriptiveTypeSignature) typeSignature).descriptor();
358+
if (descriptor instanceof Class) {
359+
fieldType = mapper.constructType((Class<?>) descriptor);
360+
}
361+
}
362+
fieldRequirement = FieldRequirement.OPTIONAL;
363+
} else {
364+
fieldRequirement = fieldRequirement(javaType, fieldType, internalName);
365+
}
366+
367+
final DescriptionInfo descriptionInfo = fieldDescriptionInfo(javaType, fieldType, internalName);
368+
return FieldInfo.builder(name, typeSignature)
369+
.requirement(fieldRequirement)
370+
.descriptionInfo(descriptionInfo)
371+
.build();
372+
}
373+
374+
private FieldRequirement fieldRequirement(JavaType classType, JavaType fieldType, String fieldName) {
375+
if (fieldType.isPrimitive()) {
376+
return FieldRequirement.REQUIRED;
377+
}
378+
379+
if (KotlinUtil.isData(classType.getRawClass())) {
380+
// Only the parameters in the constructor of data classes correctly provide
381+
// `isMarkedNullable` information.
382+
final FieldRequirement requirement =
383+
extractFromConstructor(classType, fieldType, fieldName, parameter -> {
384+
if (isNullable(parameter)) {
385+
return FieldRequirement.OPTIONAL;
386+
} else {
387+
return FieldRequirement.REQUIRED;
388+
}
389+
});
390+
if (requirement != null) {
391+
return requirement;
392+
}
393+
}
394+
395+
final FieldRequirement requirement =
396+
extractFieldMeta(classType, fieldType, fieldName, element -> {
397+
if (request) {
398+
if (element instanceof Method) {
399+
// Use the first parameter information for the setter method
400+
element = ((Method) element).getParameters()[0];
401+
}
402+
}
403+
if (isNullable(element)) {
404+
return FieldRequirement.OPTIONAL;
405+
}
406+
return FieldRequirement.REQUIRED;
407+
});
408+
409+
return firstNonNull(requirement, FieldRequirement.UNSPECIFIED);
410+
}
411+
412+
private DescriptionInfo fieldDescriptionInfo(JavaType classType, JavaType fieldType, String fieldName) {
413+
final Description description = extractFieldMeta(classType, fieldType, fieldName, element -> {
414+
return AnnotationUtil.findFirst(element, Description.class);
415+
});
416+
417+
if (description != null) {
418+
return DescriptionInfo.from(description);
419+
} else {
420+
return DescriptionInfo.empty();
421+
}
422+
}
423+
424+
@Nullable
425+
private <T> T extractFieldMeta(JavaType classType, JavaType fieldType, String fieldName,
426+
Function<AnnotatedElement, @Nullable T> extractor) {
427+
if (request) {
428+
return extractRequestFieldMeta(classType, fieldType, fieldName, extractor);
429+
} else {
430+
return extractResponseFieldMeta(classType, fieldType, fieldName, extractor);
431+
}
432+
}
433433
}

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;

0 commit comments

Comments
 (0)