Skip to content

Commit 9ce6b70

Browse files
authored
Merge branch 'master' into bug/4967-not-required-overrides-annotations
2 parents 3340e4e + 75af075 commit 9ce6b70

File tree

38 files changed

+695
-90
lines changed

38 files changed

+695
-90
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ The OpenAPI Specification has undergone several revisions since initial creation
2323

2424
Swagger core Version | Release Date | OpenAPI Spec compatibility | Notes | Status
2525
------------------------- | ------------ | -------------------------- | ----- | ----
26-
2.2.39 (**current stable**)| 2025-10-13 | 3.x | [tag v2.2.39](https://github.com/swagger-api/swagger-core/tree/v2.2.39) | Supported
26+
2.2.40 (**current stable**)| 2025-10-28 | 3.x | [tag v2.2.40](https://github.com/swagger-api/swagger-core/tree/v2.2.40) | Supported
27+
2.2.39 | 2025-10-13 | 3.x | [tag v2.2.39](https://github.com/swagger-api/swagger-core/tree/v2.2.39) | Supported
2728
2.2.38 | 2025-09-29 | 3.x | [tag v2.2.38](https://github.com/swagger-api/swagger-core/tree/v2.2.38) | Supported
2829
2.2.37 | 2025-09-16 | 3.x | [tag v2.2.37](https://github.com/swagger-api/swagger-core/tree/v2.2.37) | Supported
2930
2.2.36 | 2025-08-18 | 3.x | [tag v2.2.36](https://github.com/swagger-api/swagger-core/tree/v2.2.36) | Supported
@@ -132,7 +133,7 @@ You need the following installed and available in your $PATH:
132133
* Jackson 2.4.5 or greater
133134

134135

135-
### To build from source (currently 2.2.40-SNAPSHOT)
136+
### To build from source (currently 2.2.41-SNAPSHOT)
136137
```
137138
# first time building locally
138139
mvn -N

modules/swagger-annotations/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<parent>
44
<groupId>io.swagger.core.v3</groupId>
55
<artifactId>swagger-project</artifactId>
6-
<version>2.2.40-SNAPSHOT</version>
6+
<version>2.2.41-SNAPSHOT</version>
77
<relativePath>../..</relativePath>
88
</parent>
99
<modelVersion>4.0.0</modelVersion>

modules/swagger-core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<parent>
44
<groupId>io.swagger.core.v3</groupId>
55
<artifactId>swagger-project</artifactId>
6-
<version>2.2.40-SNAPSHOT</version>
6+
<version>2.2.41-SNAPSHOT</version>
77
<relativePath>../..</relativePath>
88
</parent>
99
<modelVersion>4.0.0</modelVersion>

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: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ public class ModelResolver extends AbstractModelConverter implements ModelConver
141141

142142
protected ValidatorProcessor validatorProcessor;
143143

144+
protected Set<AnnotatedType> typesBeingResolved = new HashSet<>();
145+
144146
public ModelResolver(ObjectMapper mapper) {
145147
super(mapper);
146148
}
@@ -787,6 +789,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
787789
.resolveAsRef(annotatedType.isResolveAsRef())
788790
.jsonViewAnnotation(annotatedType.getJsonViewAnnotation())
789791
.skipSchemaName(true)
792+
.subtype(annotatedType.isSubtype())
790793
.schemaProperty(true)
791794
.components(annotatedType.getComponents())
792795
.propertyName(propName)
@@ -828,7 +831,8 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
828831
if (reResolvedProperty.isPresent()) {
829832
property = reResolvedProperty.get();
830833
}
831-
reResolvedProperty = AnnotationsUtils.getArraySchema(ctxArraySchema, annotatedType.getComponents(), null, openapi31, property, true);
834+
835+
reResolvedProperty = resolveArraySchemaWithCycleGuard(ctxArraySchema, annotatedType, openapi31, property);
832836
if (reResolvedProperty.isPresent()) {
833837
property = reResolvedProperty.get();
834838
}
@@ -1560,6 +1564,7 @@ protected Schema processAsId(String propertyName, AnnotatedType type,
15601564
.jsonViewAnnotation(type.getJsonViewAnnotation())
15611565
.schemaProperty(true)
15621566
.components(type.getComponents())
1567+
.subtype(type.isSubtype())
15631568
.propertyName(type.getPropertyName());
15641569

15651570
return context.resolve(aType);
@@ -2110,8 +2115,10 @@ private boolean resolveSubtypes(Schema model, BeanDescription bean, ModelConvert
21102115
continue;
21112116
}
21122117

2113-
final Schema subtypeModel = context.resolve(new AnnotatedType().type(subtypeType)
2114-
.jsonViewAnnotation(jsonViewAnnotation));
2118+
final Schema subtypeModel = context.resolve(new AnnotatedType()
2119+
.type(subtypeType)
2120+
.jsonViewAnnotation(jsonViewAnnotation)
2121+
.subtype(true));
21152122

21162123
if (StringUtils.isBlank(subtypeModel.getName()) ||
21172124
subtypeModel.getName().equals(model.getName())) {
@@ -3604,4 +3611,24 @@ protected boolean applySchemaResolution() {
36043611
(Boolean.parseBoolean(System.getProperty(Schema.APPLY_SCHEMA_RESOLUTION_PROPERTY, "false")) ||
36053612
Boolean.parseBoolean(System.getenv(Schema.APPLY_SCHEMA_RESOLUTION_PROPERTY)));
36063613
}
3614+
3615+
private Optional<Schema> resolveArraySchemaWithCycleGuard(
3616+
io.swagger.v3.oas.annotations.media.ArraySchema ctxArraySchema,
3617+
AnnotatedType annotatedType,
3618+
boolean openapi31,
3619+
Schema<?> property) {
3620+
boolean processSchemaImplementation = !typesBeingResolved.contains(annotatedType);
3621+
Optional<Schema> reResolvedProperty;
3622+
if (processSchemaImplementation) {
3623+
typesBeingResolved.add(annotatedType);
3624+
} try {
3625+
reResolvedProperty = AnnotationsUtils.getArraySchema(ctxArraySchema, annotatedType.getComponents(), null,
3626+
openapi31, property, processSchemaImplementation );
3627+
} finally {
3628+
if (processSchemaImplementation) {
3629+
typesBeingResolved.remove(annotatedType);
3630+
}
3631+
}
3632+
return reResolvedProperty;
3633+
}
36073634
}

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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package io.swagger.v3.core.converting;
2+
3+
import io.swagger.v3.core.converter.ModelConverters;
4+
import io.swagger.v3.core.converter.ResolvedSchema;
5+
import io.swagger.v3.core.oas.models.ModelWithArrayOfSubclasses;
6+
import io.swagger.v3.core.util.Json31;
7+
8+
import static org.testng.Assert.assertEquals;
9+
import static org.testng.Assert.assertNotNull;
10+
import org.testng.annotations.Test;
11+
12+
import java.nio.file.Files;
13+
import java.nio.file.Paths;
14+
import com.fasterxml.jackson.databind.ObjectMapper;
15+
import com.fasterxml.jackson.databind.JsonNode;
16+
17+
18+
public class ArrayOfSubclassTest {
19+
20+
@Test
21+
public void extractSubclassArray_oas31() throws Exception {
22+
ResolvedSchema schema = ModelConverters.getInstance(true).readAllAsResolvedSchema(ModelWithArrayOfSubclasses.Holder.class);
23+
assertNotNull(schema);
24+
String expectedJson = new String(Files.readAllBytes(Paths.get("src/test/java/io/swagger/v3/core/converting/ArrayOfSubclassTest_expected31.json")));
25+
String actualJson = Json31.pretty(schema);
26+
ObjectMapper mapper = new ObjectMapper();
27+
JsonNode expectedNode = mapper.readTree(expectedJson);
28+
JsonNode actualNode = mapper.readTree(actualJson);
29+
assertEquals(actualNode, expectedNode);
30+
}
31+
32+
@Test
33+
public void extractSubclassArray_oas30() throws Exception {
34+
ResolvedSchema schema = ModelConverters.getInstance(false).readAllAsResolvedSchema(ModelWithArrayOfSubclasses.Holder.class);
35+
assertNotNull(schema);
36+
String expectedJson = new String(Files.readAllBytes(Paths.get("src/test/java/io/swagger/v3/core/converting/ArrayOfSubclassTest_expected30.json")));
37+
String actualJson = Json31.pretty(schema);
38+
ObjectMapper mapper = new ObjectMapper();
39+
JsonNode expectedNode = mapper.readTree(expectedJson);
40+
JsonNode actualNode = mapper.readTree(actualJson);
41+
assertEquals(actualNode, expectedNode);
42+
}
43+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"schema" : {
3+
"description" : "The holder",
4+
"properties" : {
5+
"name" : {
6+
"type" : "string"
7+
},
8+
"friend" : {
9+
"type" : "string"
10+
},
11+
"baseArray" : {
12+
"type" : "array",
13+
"description" : "Thingy",
14+
"items" : {
15+
"$ref" : "#/components/schemas/Base"
16+
},
17+
"minItems" : 0,
18+
"uniqueItems" : true
19+
}
20+
}
21+
},
22+
"referencedSchemas" : {
23+
"Base" : {
24+
"description" : "Stuff",
25+
"discriminator" : {
26+
"propertyName" : "name",
27+
"mapping" : {
28+
"a" : "#/components/schemas/SubA",
29+
"b" : "#/components/schemas/SubB"
30+
}
31+
},
32+
"properties" : {
33+
"name" : {
34+
"type" : "string"
35+
}
36+
}
37+
},
38+
"Holder" : {
39+
"description" : "The holder",
40+
"properties" : {
41+
"name" : {
42+
"type" : "string"
43+
},
44+
"friend" : {
45+
"type" : "string"
46+
},
47+
"baseArray" : {
48+
"type" : "array",
49+
"description" : "Thingy",
50+
"items" : {
51+
"$ref" : "#/components/schemas/Base"
52+
},
53+
"minItems" : 0,
54+
"uniqueItems" : true
55+
}
56+
}
57+
},
58+
"SubA" : {
59+
"description" : "The SubA class",
60+
"properties" : {
61+
"name" : {
62+
"type" : "string"
63+
},
64+
"count" : {
65+
"type" : "integer",
66+
"format" : "int64"
67+
}
68+
}
69+
},
70+
"SubB" : {
71+
"description" : "The SubB class",
72+
"properties" : {
73+
"name" : {
74+
"type" : "string"
75+
},
76+
"friend" : {
77+
"type" : "string"
78+
}
79+
}
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)