Skip to content

Commit c13f6fc

Browse files
feat: infer discriminator mapping
1 parent e7387df commit c13f6fc

16 files changed

Lines changed: 509 additions & 45 deletions

modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java

Lines changed: 127 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
import com.fasterxml.jackson.databind.BeanDescription;
1717
import com.fasterxml.jackson.databind.JavaType;
1818
import com.fasterxml.jackson.databind.JsonNode;
19+
import com.fasterxml.jackson.databind.JsonMappingException;
1920
import com.fasterxml.jackson.databind.ObjectMapper;
2021
import com.fasterxml.jackson.databind.PropertyMetadata;
22+
import com.fasterxml.jackson.databind.SerializationConfig;
2123
import com.fasterxml.jackson.databind.SerializationFeature;
2224
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
2325
import com.fasterxml.jackson.databind.introspect.Annotated;
@@ -28,6 +30,8 @@
2830
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
2931
import com.fasterxml.jackson.databind.introspect.POJOPropertyBuilder;
3032
import com.fasterxml.jackson.databind.jsontype.NamedType;
33+
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
34+
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
3135
import com.fasterxml.jackson.databind.util.Annotations;
3236
import io.swagger.v3.core.converter.AnnotatedType;
3337
import io.swagger.v3.core.converter.ModelConverter;
@@ -967,7 +971,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
967971
model.setDiscriminator(null);
968972
}
969973

970-
Discriminator discriminator = resolveDiscriminator(type, context);
974+
Discriminator discriminator = resolveDiscriminator(model, beanDesc, context, annotatedType.getJsonViewAnnotation());
971975
if (discriminator != null) {
972976
model.setDiscriminator(discriminator);
973977
}
@@ -2668,22 +2672,63 @@ protected Map<String, Object> resolveExtensions(Annotated a, Annotation[] annota
26682672
return null;
26692673
}
26702674

2675+
protected TypeSerializer getTypeSerializer(JavaType type) {
2676+
SerializationConfig config = _mapper.getSerializationConfig();
2677+
2678+
try {
2679+
return _mapper.getSerializerFactory().createTypeSerializer(
2680+
config, type
2681+
);
2682+
} catch (JsonMappingException e) {
2683+
LOGGER.error("Unable to create type serializer", e);
2684+
}
2685+
2686+
return null;
2687+
}
2688+
2689+
protected TypeSerializer getTypeSerializerForDiscriminatorInference(JavaType type) {
2690+
// longer method would involve AnnotationIntrospector.findTypeResolver(...) but:
2691+
JsonTypeInfo typeInfo = type.getRawClass().getDeclaredAnnotation(JsonTypeInfo.class);
2692+
if (typeInfo == null || typeInfo.use() == JsonTypeInfo.Id.NONE) {
2693+
return null;
2694+
}
2695+
2696+
TypeSerializer serializer = getTypeSerializer(type);
2697+
if (serializer == null) {
2698+
return null;
2699+
}
2700+
2701+
JsonTypeInfo.As include = serializer.getTypeInclusion();
2702+
if (
2703+
!(
2704+
include == JsonTypeInfo.As.PROPERTY
2705+
|| include == JsonTypeInfo.As.EXISTING_PROPERTY
2706+
|| include == JsonTypeInfo.As.WRAPPER_OBJECT
2707+
)
2708+
) {
2709+
return null;
2710+
}
2711+
2712+
return serializer;
2713+
}
2714+
26712715
protected void resolveDiscriminatorProperty(JavaType type, ModelConverterContext context, Schema model) {
26722716
// add JsonTypeInfo.property if not member of bean
2673-
JsonTypeInfo typeInfo = type.getRawClass().getDeclaredAnnotation(JsonTypeInfo.class);
2674-
if (typeInfo != null) {
2675-
String typeInfoProp = typeInfo.property();
2676-
if (StringUtils.isNotBlank(typeInfoProp)) {
2677-
Schema modelToUpdate = model;
2678-
if (StringUtils.isNotBlank(model.get$ref())) {
2679-
modelToUpdate = context.getDefinedModels().get(model.get$ref().substring(SCHEMA_COMPONENT_PREFIX));
2680-
}
2681-
if (modelToUpdate.getProperties() == null || !modelToUpdate.getProperties().keySet().contains(typeInfoProp)) {
2682-
Schema discriminatorSchema = openapi31 ? new JsonSchema().typesItem("string").name(typeInfoProp) : new StringSchema().name(typeInfoProp);
2683-
modelToUpdate.addProperties(typeInfoProp, discriminatorSchema);
2684-
if (modelToUpdate.getRequired() == null || !modelToUpdate.getRequired().contains(typeInfoProp)) {
2685-
modelToUpdate.addRequiredItem(typeInfoProp);
2686-
}
2717+
TypeSerializer serializer = getTypeSerializerForDiscriminatorInference(type);
2718+
if (serializer == null) {
2719+
return;
2720+
}
2721+
String propertyName = serializer.getPropertyName();
2722+
if (StringUtils.isNotBlank(propertyName)) {
2723+
Schema modelToUpdate = model;
2724+
if (StringUtils.isNotBlank(model.get$ref())) {
2725+
modelToUpdate = context.getDefinedModels().get(model.get$ref().substring(SCHEMA_COMPONENT_PREFIX));
2726+
}
2727+
if (modelToUpdate.getProperties() == null || !modelToUpdate.getProperties().keySet().contains(propertyName)) {
2728+
Schema discriminatorSchema = openapi31 ? new JsonSchema().typesItem("string").name(propertyName) : new StringSchema().name(propertyName);
2729+
modelToUpdate.addProperties(propertyName, discriminatorSchema);
2730+
if (modelToUpdate.getRequired() == null || !modelToUpdate.getRequired().contains(propertyName)) {
2731+
modelToUpdate.addRequiredItem(propertyName);
26872732
}
26882733
}
26892734
}
@@ -2722,23 +2767,23 @@ protected Schema resolveWrapping(JavaType type, ModelConverterContext context, S
27222767
return model;
27232768
}
27242769

2725-
protected Discriminator resolveDiscriminator(JavaType type, ModelConverterContext context) {
2770+
protected Discriminator resolveDiscriminator(Schema model, BeanDescription bean, ModelConverterContext context, JsonView jsonViewAnnotation) {
2771+
io.swagger.v3.oas.annotations.media.Schema declaredSchemaAnnotation = AnnotationsUtils.getSchemaDeclaredAnnotation(bean.getType().getRawClass());
27262772

2727-
io.swagger.v3.oas.annotations.media.Schema declaredSchemaAnnotation = AnnotationsUtils.getSchemaDeclaredAnnotation(type.getRawClass());
2773+
if (declaredSchemaAnnotation != null) {
2774+
String propertyName = declaredSchemaAnnotation.discriminatorProperty();
27282775

2729-
String disc = (declaredSchemaAnnotation == null) ? "" : declaredSchemaAnnotation.discriminatorProperty();
2730-
2731-
if (disc.isEmpty()) {
2732-
// longer method would involve AnnotationIntrospector.findTypeResolver(...) but:
2733-
JsonTypeInfo typeInfo = type.getRawClass().getDeclaredAnnotation(JsonTypeInfo.class);
2734-
if (typeInfo != null) {
2735-
disc = typeInfo.property();
2776+
if (StringUtils.isBlank(propertyName)) {
2777+
TypeSerializer serializer = getTypeSerializerForDiscriminatorInference(bean.getType());
2778+
if (serializer == null) {
2779+
return null;
2780+
}
2781+
propertyName = serializer.getPropertyName();
27362782
}
2737-
}
2738-
if (!disc.isEmpty()) {
2739-
Discriminator discriminator = new Discriminator()
2740-
.propertyName(disc);
2741-
if (declaredSchemaAnnotation != null) {
2783+
2784+
if (StringUtils.isNotBlank(propertyName)) {
2785+
Discriminator discriminator = new Discriminator().propertyName(propertyName);
2786+
27422787
DiscriminatorMapping[] mappings = declaredSchemaAnnotation.discriminatorMapping();
27432788
if (mappings != null && mappings.length > 0) {
27442789
for (DiscriminatorMapping mapping : mappings) {
@@ -2747,11 +2792,62 @@ protected Discriminator resolveDiscriminator(JavaType type, ModelConverterContex
27472792
}
27482793
}
27492794
}
2795+
2796+
return discriminator;
27502797
}
2798+
}
27512799

2752-
return discriminator;
2800+
TypeSerializer serializer = getTypeSerializerForDiscriminatorInference(bean.getType());
2801+
if (serializer == null) {
2802+
return null;
27532803
}
2754-
return null;
2804+
String propertyName = serializer.getPropertyName();
2805+
if (StringUtils.isBlank(propertyName)) {
2806+
return null;
2807+
}
2808+
2809+
Discriminator discriminator = new Discriminator().propertyName(propertyName);
2810+
2811+
// Use same approach to finding subtypes as resolveSubtypes, mimicking
2812+
final List<NamedType> subTypes = _intr().findSubtypes(bean.getClassInfo());
2813+
if (subTypes == null) {
2814+
return null;
2815+
}
2816+
2817+
removeSuperClassAndInterfaceSubTypes(subTypes, bean);
2818+
2819+
final Class<?> beanClass = bean.getClassInfo().getAnnotated();
2820+
if (!subTypes.isEmpty()) {
2821+
TypeIdResolver resolver = serializer.getTypeIdResolver();
2822+
2823+
for (NamedType subType : subTypes) {
2824+
final Class<?> subtypeType = subType.getType();
2825+
if (!beanClass.isAssignableFrom(subtypeType)) {
2826+
continue;
2827+
}
2828+
2829+
String subTypeName = resolver.idFromValueAndType(null, subType.getType());
2830+
2831+
final Schema subtypeModel = context.resolve(new AnnotatedType()
2832+
.type(subtypeType)
2833+
.jsonViewAnnotation(jsonViewAnnotation)
2834+
.subtype(true));
2835+
2836+
if (StringUtils.isBlank(subtypeModel.getName()) ||
2837+
subtypeModel.getName().equals(model.getName())) {
2838+
subtypeModel.setName(_typeNameResolver.nameForType(_mapper.constructType(subtypeType),
2839+
TypeNameResolver.Options.SKIP_API_MODEL));
2840+
}
2841+
2842+
// Per the specification, there is an implicit map to schemas with the same name
2843+
// We skip writing the mappings that are implied to keep the schema minimal
2844+
if (!subTypeName.equals(subtypeModel.getName())) {
2845+
discriminator.mapping(subTypeName, RefUtils.constructRef(subtypeModel.getName()));
2846+
}
2847+
}
2848+
}
2849+
2850+
return discriminator;
27552851
}
27562852

27572853
protected XML resolveXml(Annotated a, Annotation[] annotations, io.swagger.v3.oas.annotations.media.Schema schema) {

modules/swagger-core/src/test/java/io/swagger/v3/core/converting/CompositionTest.java

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22

33
import io.swagger.v3.core.converter.ModelConverters;
44
import io.swagger.v3.core.matchers.SerializationMatchers;
5-
import io.swagger.v3.core.oas.models.composition.AbstractBaseModelWithoutFields;
6-
import io.swagger.v3.core.oas.models.composition.Animal;
7-
import io.swagger.v3.core.oas.models.composition.AnimalClass;
8-
import io.swagger.v3.core.oas.models.composition.AnimalWithSchemaSubtypes;
9-
import io.swagger.v3.core.oas.models.composition.Human;
10-
import io.swagger.v3.core.oas.models.composition.ModelWithFieldWithSubTypes;
5+
import io.swagger.v3.core.oas.models.composition.*;
6+
import io.swagger.v3.core.oas.models.composition.discriminator.ModelWithDefaultPropertyName;
7+
import io.swagger.v3.core.oas.models.composition.discriminator.ModelWithDiscriminatorMapping;
8+
import io.swagger.v3.core.oas.models.composition.discriminator.ModelWithProvidedDiscriminatorMapping;
9+
import io.swagger.v3.core.oas.models.composition.discriminator.ModelWithoutTypeInfo;
1110
import io.swagger.v3.core.util.Json;
1211
import io.swagger.v3.core.util.ResourceUtils;
1312
import io.swagger.v3.oas.models.media.Schema;
@@ -48,6 +47,26 @@ public void createModelWithFieldWithSubTypes() throws IOException {
4847
compareAsJson(ModelWithFieldWithSubTypes.class, "ModelWithFieldWithSubTypes.json");
4948
}
5049

50+
@Test(description = "create a ModelWithDiscriminatorMapping")
51+
public void createModelWithDiscriminatorMapping() throws IOException {
52+
compareAsJson(ModelWithDiscriminatorMapping.class, "ModelWithDiscriminatorMapping.json");
53+
}
54+
55+
@Test(description = "create a ModelWithDefaultProperty")
56+
public void createModelWithDefaultProperty() throws IOException {
57+
compareAsJson(ModelWithDefaultPropertyName.class, "ModelWithDefaultProperty.json");
58+
}
59+
60+
@Test(description = "create a ModelWithoutTypeInfo")
61+
public void createModelWithoutTypeInfo() throws IOException {
62+
compareAsJson(ModelWithoutTypeInfo.class, "ModelWithoutTypeInfo.json");
63+
}
64+
65+
@Test(description = "create a createModelWithProvidedDiscriminatorMapping")
66+
public void createModelWithProvidedDiscriminatorMapping() throws IOException {
67+
compareAsJson(ModelWithProvidedDiscriminatorMapping.class, "ModelWithProvidedDiscriminatorMapping.json");
68+
}
69+
5170
private void compareAsJson(Class<?> cls, String fileName) throws IOException {
5271
final Map<String, Schema> schemas = ModelConverters.getInstance().readAll(cls);
5372
Json.prettyPrint(schemas);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package io.swagger.v3.core.oas.models.composition.discriminator;
2+
3+
import com.fasterxml.jackson.annotation.JsonSubTypes;
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
5+
import com.fasterxml.jackson.annotation.JsonTypeName;
6+
7+
@JsonTypeInfo(use= JsonTypeInfo.Id.SIMPLE_NAME, include = JsonTypeInfo.As.PROPERTY)
8+
@JsonSubTypes({
9+
@JsonSubTypes.Type(value = ModelWithDefaultPropertyName.First.class),
10+
@JsonSubTypes.Type(value = ModelWithDefaultPropertyName.Second.class, name = "SecondType"),
11+
})
12+
public class ModelWithDefaultPropertyName {
13+
14+
public static class First extends ModelWithDefaultPropertyName {
15+
private String name;
16+
17+
public String getName() { return name; }
18+
19+
public void setName(String name) { this.name = name; }
20+
}
21+
22+
public static class Second extends ModelWithDefaultPropertyName {
23+
private String value;
24+
25+
public String getValue() { return value; }
26+
27+
public void setValue(String value) { this.value = value; }
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package io.swagger.v3.core.oas.models.composition.discriminator;
2+
3+
import com.fasterxml.jackson.annotation.JsonSubTypes;
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
5+
import com.fasterxml.jackson.annotation.JsonTypeName;
6+
7+
@JsonTypeInfo(use= JsonTypeInfo.Id.SIMPLE_NAME, include = JsonTypeInfo.As.PROPERTY, property="kind")
8+
@JsonSubTypes({
9+
@JsonSubTypes.Type(value = ModelWithDiscriminatorMapping.First.class),
10+
@JsonSubTypes.Type(value = ModelWithDiscriminatorMapping.Second.class, name = "SecondType"),
11+
@JsonSubTypes.Type(value = ModelWithDiscriminatorMapping.Third.class)
12+
})
13+
public class ModelWithDiscriminatorMapping {
14+
15+
public static class First extends ModelWithDiscriminatorMapping {
16+
private String name;
17+
18+
public String getName() { return name; }
19+
20+
public void setName(String name) { this.name = name; }
21+
}
22+
23+
public static class Second extends ModelWithDiscriminatorMapping {
24+
private String value;
25+
26+
public String getValue() { return value; }
27+
28+
public void setValue(String value) { this.value = value; }
29+
}
30+
31+
@JsonTypeName("ThirdType")
32+
public static class Third extends ModelWithDiscriminatorMapping {
33+
private String value;
34+
35+
public String getValue() { return value; }
36+
37+
public void setValue(String value) { this.value = value; }
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package io.swagger.v3.core.oas.models.composition.discriminator;
2+
3+
import com.fasterxml.jackson.annotation.JsonSubTypes;
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
5+
import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
6+
import io.swagger.v3.oas.annotations.media.Schema;
7+
8+
@JsonTypeInfo(use= JsonTypeInfo.Id.SIMPLE_NAME, include = JsonTypeInfo.As.PROPERTY, property="kind")
9+
@JsonSubTypes({
10+
@JsonSubTypes.Type(value = ModelWithProvidedDiscriminatorMapping.First.class),
11+
@JsonSubTypes.Type(value = ModelWithProvidedDiscriminatorMapping.Second.class),
12+
})
13+
@Schema(
14+
discriminatorMapping = {
15+
@DiscriminatorMapping(schema = ModelWithProvidedDiscriminatorMapping.First.class, value = "FirstOne"),
16+
@DiscriminatorMapping(schema = ModelWithProvidedDiscriminatorMapping.Second.class, value = "SecondOne"),
17+
}
18+
)
19+
public class ModelWithProvidedDiscriminatorMapping {
20+
21+
public static class First extends ModelWithProvidedDiscriminatorMapping {
22+
private String name;
23+
24+
public String getName() { return name; }
25+
26+
public void setName(String name) { this.name = name; }
27+
}
28+
29+
public static class Second extends ModelWithProvidedDiscriminatorMapping {
30+
private String value;
31+
32+
public String getValue() { return value; }
33+
34+
public void setValue(String value) { this.value = value; }
35+
}
36+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.swagger.v3.core.oas.models.composition.discriminator;
2+
3+
import com.fasterxml.jackson.annotation.JsonSubTypes;
4+
import com.fasterxml.jackson.annotation.JsonTypeInfo;
5+
6+
@JsonTypeInfo(use= JsonTypeInfo.Id.NONE)
7+
@JsonSubTypes({
8+
@JsonSubTypes.Type(value = ModelWithoutTypeInfo.First.class),
9+
@JsonSubTypes.Type(value = ModelWithoutTypeInfo.Second.class, name = "SecondType"),
10+
})
11+
public class ModelWithoutTypeInfo {
12+
13+
public static class First extends ModelWithoutTypeInfo {
14+
private String name;
15+
16+
public String getName() { return name; }
17+
18+
public void setName(String name) { this.name = name; }
19+
}
20+
21+
public static class Second extends ModelWithoutTypeInfo {
22+
private String value;
23+
24+
public String getValue() { return value; }
25+
26+
public void setValue(String value) { this.value = value; }
27+
}
28+
}

0 commit comments

Comments
 (0)