Skip to content

Commit 8260e8f

Browse files
authored
Fix/annotated type caching 5003 (#5005)
1 parent 99ba4cd commit 8260e8f

File tree

4 files changed

+255
-4
lines changed

4 files changed

+255
-4
lines changed

modules/swagger-core/src/main/java/io/swagger/v3/core/converter/AnnotatedType.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class AnnotatedType {
2929
private boolean skipSchemaName;
3030
private boolean skipJsonIdentity;
3131
private String propertyName;
32+
private boolean isSubtype;
3233

3334
private Components components;
3435

@@ -243,6 +244,19 @@ public AnnotatedType propertyName(String propertyName) {
243244
return this;
244245
}
245246

247+
public boolean isSubtype() {
248+
return isSubtype;
249+
}
250+
251+
public void setSubtype(boolean isSubtype) {
252+
this.isSubtype = isSubtype;
253+
}
254+
255+
public AnnotatedType subtype(boolean isSubtype) {
256+
this.isSubtype = isSubtype;
257+
return this;
258+
}
259+
246260
private List<Annotation> getProcessedAnnotations(Annotation[] annotations) {
247261
if (annotations == null || annotations.length == 0) {
248262
return new ArrayList<>();
@@ -264,14 +278,17 @@ public boolean equals(Object o) {
264278
List<Annotation> thisAnnotatinons = getProcessedAnnotations(this.ctxAnnotations);
265279
List<Annotation> thatAnnotatinons = getProcessedAnnotations(that.ctxAnnotations);
266280
return includePropertiesWithoutJSONView == that.includePropertiesWithoutJSONView &&
281+
schemaProperty == that.schemaProperty &&
282+
isSubtype == that.isSubtype &&
267283
Objects.equals(type, that.type) &&
268284
Objects.equals(thisAnnotatinons, thatAnnotatinons) &&
269-
Objects.equals(jsonViewAnnotation, that.jsonViewAnnotation);
285+
Objects.equals(jsonViewAnnotation, that.jsonViewAnnotation) &&
286+
(!schemaProperty || Objects.equals(propertyName, that.propertyName));
270287
}
271288

272289
@Override
273290
public int hashCode() {
274291
List<Annotation> processedAnnotations = getProcessedAnnotations(this.ctxAnnotations);
275-
return Objects.hash(type, jsonViewAnnotation, includePropertiesWithoutJSONView, processedAnnotations);
292+
return Objects.hash(type, jsonViewAnnotation, includePropertiesWithoutJSONView, processedAnnotations, schemaProperty, isSubtype, schemaProperty ? propertyName : null);
276293
}
277294
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
787787
.resolveAsRef(annotatedType.isResolveAsRef())
788788
.jsonViewAnnotation(annotatedType.getJsonViewAnnotation())
789789
.skipSchemaName(true)
790+
.subtype(annotatedType.isSubtype())
790791
.schemaProperty(true)
791792
.components(annotatedType.getComponents())
792793
.propertyName(propName)
@@ -1560,6 +1561,7 @@ protected Schema processAsId(String propertyName, AnnotatedType type,
15601561
.jsonViewAnnotation(type.getJsonViewAnnotation())
15611562
.schemaProperty(true)
15621563
.components(type.getComponents())
1564+
.subtype(type.isSubtype())
15631565
.propertyName(type.getPropertyName());
15641566

15651567
return context.resolve(aType);
@@ -2110,8 +2112,10 @@ private boolean resolveSubtypes(Schema model, BeanDescription bean, ModelConvert
21102112
continue;
21112113
}
21122114

2113-
final Schema subtypeModel = context.resolve(new AnnotatedType().type(subtypeType)
2114-
.jsonViewAnnotation(jsonViewAnnotation));
2115+
final Schema subtypeModel = context.resolve(new AnnotatedType()
2116+
.type(subtypeType)
2117+
.jsonViewAnnotation(jsonViewAnnotation)
2118+
.subtype(true));
21152119

21162120
if (StringUtils.isBlank(subtypeModel.getName()) ||
21172121
subtypeModel.getName().equals(model.getName())) {

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,50 @@ class SubAnnotatedType extends AnnotatedType {
102102
assertEquals(parent.hashCode(), child.hashCode(), "Parent and child hash codes should be equal if their properties are the same.");
103103
assertNotEquals(parent, differentParent, "Objects with different properties should not be equal.");
104104
}
105+
106+
@Test
107+
public void testEquals_shouldDifferentiatePropertyAndSubtypeContexts() {
108+
AnnotatedType typeAsProperty = new AnnotatedType(String.class)
109+
.schemaProperty(false)
110+
.propertyName("fieldA");
111+
112+
AnnotatedType typeAsSubtype = new AnnotatedType(String.class)
113+
.schemaProperty(true)
114+
.propertyName(null);
115+
116+
assertNotEquals(typeAsProperty, typeAsSubtype,
117+
"Objects with different schemaProperty flags must not be equal.");
118+
assertNotEquals(typeAsProperty.hashCode(), typeAsSubtype.hashCode(),
119+
"Hash codes must be different if schemaProperty flags differ.");
120+
}
121+
122+
@Test
123+
public void testEquals_shouldComparePropertyNameWhenSchemaPropertyIsTrue() {
124+
AnnotatedType complexPropA = new AnnotatedType(String.class)
125+
.schemaProperty(true)
126+
.propertyName("fieldA");
127+
AnnotatedType complexPropB = new AnnotatedType(String.class)
128+
.schemaProperty(true)
129+
.propertyName("fieldB");
130+
131+
assertNotEquals(complexPropA, complexPropB,
132+
"When schemaProperty is true, objects with different propertyNames must not be equal.");
133+
assertNotEquals(complexPropA.hashCode(), complexPropB.hashCode(),
134+
"When schemaProperty is true, hash codes must be different if propertyNames differ.");
135+
}
136+
137+
@Test
138+
public void testEquals_shouldBeEqualWhenSchemaPropertyIsTrueAndNamesMatch() {
139+
AnnotatedType complexPropA = new AnnotatedType(String.class)
140+
.schemaProperty(true)
141+
.propertyName("fieldA");
142+
AnnotatedType complexPropC = new AnnotatedType(String.class)
143+
.schemaProperty(true)
144+
.propertyName("fieldA");
145+
146+
assertEquals(complexPropA, complexPropC,
147+
"When schemaProperty is true, objects with the same propertyName must be equal.");
148+
assertEquals(complexPropA.hashCode(), complexPropC.hashCode(),
149+
"When schemaProperty is true, hash codes must be equal if propertyNames are the same.");
150+
}
105151
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package io.swagger.v3.core.converting;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import io.swagger.v3.core.converter.AnnotatedType;
5+
import io.swagger.v3.core.converter.ModelConverterContextImpl;
6+
import io.swagger.v3.core.jackson.ModelResolver;
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import io.swagger.v3.oas.models.media.ComposedSchema;
9+
import org.testng.annotations.Test;
10+
11+
import javax.validation.constraints.NotNull;
12+
import java.lang.reflect.Field;
13+
import java.util.List;
14+
import java.util.Map;
15+
16+
import static org.testng.Assert.*;
17+
import static org.testng.AssertJUnit.assertEquals;
18+
19+
public class PolymorphicSubtypePropertyBleedTest {
20+
21+
@Schema(
22+
description = "base",
23+
discriminatorProperty = "kind",
24+
oneOf = { ConcretionA.class }
25+
)
26+
public static class BaseResponse {
27+
public String sharedField;
28+
public String kind;
29+
}
30+
31+
@Schema(description = "subtype A")
32+
public static class ConcretionA extends BaseResponse {
33+
public String onlyInA;
34+
}
35+
36+
public static class Wrapper {
37+
@NotNull
38+
public ConcretionA concretionA;
39+
}
40+
41+
/**
42+
* Mimics how swagger-core resolves a bean property:
43+
* - schemaProperty=true
44+
* - propertyName set
45+
* - ctxAnnotations from the backing field (e.g. @NotNull)
46+
*/
47+
private io.swagger.v3.oas.models.media.Schema resolveAsBeanProperty(
48+
Class<?> clazz,
49+
String propertyName,
50+
Field backingField,
51+
ModelConverterContextImpl ctx,
52+
ModelResolver resolver
53+
) {
54+
AnnotatedType at = new AnnotatedType()
55+
.type(clazz)
56+
.schemaProperty(true)
57+
.propertyName(propertyName)
58+
.ctxAnnotations(backingField.getAnnotations());
59+
return ctx.resolve(at);
60+
}
61+
62+
/**
63+
* Mimics how resolveSubtypes(...) resolves a polymorphic subtype:
64+
* - NO schemaProperty(true)
65+
* - NO propertyName
66+
* - NO ctxAnnotations from some containing field
67+
68+
*/
69+
private io.swagger.v3.oas.models.media.Schema resolveAsPolymorphicSubtype(
70+
Class<?> clazz,
71+
ModelConverterContextImpl ctx,
72+
ModelResolver resolver
73+
) {
74+
AnnotatedType at = new AnnotatedType()
75+
.type(clazz);
76+
return ctx.resolve(at);
77+
}
78+
79+
/**
80+
* Baseline for comparison: new context, clean standalone schema resolution.
81+
*/
82+
private io.swagger.v3.oas.models.media.Schema resolveAsStandaloneSchema(
83+
Class<?> clazz,
84+
ModelResolver resolver
85+
) {
86+
ModelConverterContextImpl freshCtx = new ModelConverterContextImpl(resolver);
87+
AnnotatedType at = new AnnotatedType()
88+
.type(clazz);
89+
return freshCtx.resolve(at);
90+
}
91+
92+
@Test
93+
public void subtypeResolution_shouldMatchStandalone_andNotBleedFromPropertyContext() throws Exception {
94+
ObjectMapper mapper = new ObjectMapper();
95+
ModelResolver resolver = new ModelResolver(mapper);
96+
97+
ModelConverterContextImpl sharedCtx = new ModelConverterContextImpl(resolver);
98+
99+
// 1. Resolve ConcretionA in the bean property context
100+
Field wrapperField = Wrapper.class.getDeclaredField("concretionA");
101+
io.swagger.v3.oas.models.media.Schema concretionAsPropertySchema =
102+
resolveAsBeanProperty(
103+
ConcretionA.class,
104+
"concretionA",
105+
wrapperField,
106+
sharedCtx,
107+
resolver
108+
);
109+
110+
assertNotNull(concretionAsPropertySchema, "bean property resolution returned null");
111+
112+
// 2. Resolve ConcretionA again, but this time as a polymorphic subtype using the SAME sharedCtx
113+
io.swagger.v3.oas.models.media.Schema concretionAsPolymorphicSchema =
114+
resolveAsPolymorphicSubtype(
115+
ConcretionA.class,
116+
sharedCtx,
117+
resolver
118+
);
119+
assertNotNull(concretionAsPolymorphicSchema, "polymorphic subtype resolution returned null");
120+
121+
// 3. Resolve ConcretionA in a clean context to represent the canonical standalone subtype schema.
122+
io.swagger.v3.oas.models.media.Schema concretionStandaloneSchema =
123+
resolveAsStandaloneSchema(
124+
ConcretionA.class,
125+
resolver
126+
);
127+
assertNotNull(concretionStandaloneSchema, "standalone subtype resolution returned null");
128+
129+
// nullable should be consistent between standalone and subtype
130+
assertEquals(
131+
concretionStandaloneSchema.getNullable(),
132+
concretionAsPolymorphicSchema.getNullable()
133+
);
134+
135+
// required list should match between standalone and subtype-in-sharedCtx
136+
List<?> requiredStandalone = concretionStandaloneSchema.getRequired();
137+
List<?> requiredPolymorphic = concretionAsPolymorphicSchema.getRequired();
138+
if (requiredStandalone == null) requiredStandalone = java.util.Collections.emptyList();
139+
if (requiredPolymorphic == null) requiredPolymorphic = java.util.Collections.emptyList();
140+
assertEquals(
141+
requiredStandalone,
142+
requiredPolymorphic
143+
);
144+
145+
// Name should match standalone. We don't want subtype schemas
146+
assertEquals(
147+
concretionStandaloneSchema.getName(),
148+
concretionAsPolymorphicSchema.getName()
149+
);
150+
151+
//Properties should still include subtype-specific fields like 'onlyInA'.
152+
Map<?, ?> propsStandalone = concretionStandaloneSchema.getProperties();
153+
Map<?, ?> propsPolymorphic = concretionAsPolymorphicSchema.getProperties();
154+
assertNotNull(propsPolymorphic);
155+
assertTrue(propsPolymorphic.containsKey("onlyInA"));
156+
assertEquals(
157+
propsStandalone == null
158+
? java.util.Collections.emptySet()
159+
: propsStandalone.keySet(),
160+
propsPolymorphic.keySet()
161+
);
162+
163+
assertNotSame(
164+
concretionAsPropertySchema,
165+
concretionAsPolymorphicSchema
166+
);
167+
}
168+
169+
170+
@Test
171+
public void polymorphicSubtype_mustContainSubtypeFragment_notJustBaseRef() {
172+
ObjectMapper mapper = new ObjectMapper();
173+
ModelResolver resolver = new ModelResolver(mapper);
174+
ModelConverterContextImpl ctx = new ModelConverterContextImpl(resolver);
175+
176+
AnnotatedType subtypeAT = new AnnotatedType()
177+
.type(ConcretionA.class)
178+
.schemaProperty(true);
179+
180+
io.swagger.v3.oas.models.media.Schema resolvedSubtypeSchema = ctx.resolve(subtypeAT);
181+
182+
assertTrue(resolvedSubtypeSchema.getProperties().containsKey("onlyInA"));
183+
}
184+
}

0 commit comments

Comments
 (0)