Skip to content

Commit f93aee0

Browse files
authored
fix: treat streams with arraySchema annotation as array type (#5013) (#5064)
1 parent 0daf49b commit f93aee0

3 files changed

Lines changed: 337 additions & 23 deletions

File tree

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

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108

109109
import static io.swagger.v3.core.jackson.JAXBAnnotationsHelper.JAXB_DEFAULT;
110110
import static io.swagger.v3.core.util.RefUtils.constructRef;
111+
import static io.swagger.v3.core.util.SiblingAnnotationFilter.filterSiblingAnnotations;
111112
import static io.swagger.v3.core.util.ValidationAnnotationsUtils.*;
112113
import static io.swagger.v3.oas.annotations.media.Schema.DEFAULT_SENTINEL;
113114

@@ -460,6 +461,8 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
460461
return context.resolve(aType);
461462
}
462463

464+
boolean isStreamWithArrayAnnotation = resolvedArrayAnnotation != null && isStreamType(type);
465+
463466
if (type.isContainerType()) {
464467
// TODO currently a MapSchema or ArraySchema don't also support composed schema props (oneOf,..)
465468
hasCompositionKeywords = false;
@@ -525,7 +528,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
525528
mapModel.name(name);
526529
model = mapModel;
527530
} else if (valueType != null) {
528-
if (ReflectionUtils.isSystemTypeNotArray(type) && !annotatedType.isSchemaProperty() && !annotatedType.isResolveAsRef()) {
531+
if (!isStreamWithArrayAnnotation && ReflectionUtils.isSystemTypeNotArray(type) && !annotatedType.isSchemaProperty() && !annotatedType.isResolveAsRef()) {
529532
context.resolve(new AnnotatedType().components(annotatedType.getComponents()).type(valueType).jsonViewAnnotation(annotatedType.getJsonViewAnnotation()));
530533
return null;
531534
}
@@ -747,21 +750,10 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
747750
io.swagger.v3.oas.annotations.media.Schema.AccessMode accessMode = resolveAccessMode(propDef, type, propResolvedSchemaAnnotation);
748751
io.swagger.v3.oas.annotations.media.Schema.RequiredMode requiredMode = resolveRequiredMode(propResolvedSchemaAnnotation, propType);
749752

750-
Annotation[] ctxAnnotation31 = null;
751-
Schema.SchemaResolution resolvedSchemaResolution = AnnotationsUtils.resolveSchemaResolution(this.schemaResolution, ctxSchema);
752-
if (AnnotationsUtils.areSiblingsAllowed(resolvedSchemaResolution, openapi31)) {
753-
List<Annotation> ctxAnnotations31List = new ArrayList<>();
754-
if (annotations != null) {
755-
for (Annotation a : annotations) {
756-
if (
757-
!(a instanceof io.swagger.v3.oas.annotations.media.Schema) &&
758-
!(a instanceof io.swagger.v3.oas.annotations.media.ArraySchema)) {
759-
ctxAnnotations31List.add(a);
760-
}
761-
}
762-
ctxAnnotation31 = ctxAnnotations31List.toArray(new Annotation[ctxAnnotations31List.size()]);
763-
}
764-
}
753+
SiblingAnnotationFilter.FilterResult filterResult = filterSiblingAnnotations(
754+
annotations, propType, ctxSchema, ctxArraySchema, this.schemaResolution, openapi31);
755+
Annotation[] ctxFilteredSiblingAnnotations = filterResult.getFilteredAnnotations();
756+
Schema.SchemaResolution resolvedSchemaResolution = filterResult.getResolvedSchemaResolution();
765757
Set<Annotation> validationInvocationAnnotations = null;
766758
if (validatorProcessor != null) {
767759
validationInvocationAnnotations = validatorProcessor.resolveInvocationAnnotations(annotations);
@@ -776,8 +768,8 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
776768
validationInvocationAnnotations.addAll(resolveValidationInvocationAnnotations(annotatedType.getCtxAnnotations()));
777769
}
778770
annotations = Stream.concat(Arrays.stream(annotations), Arrays.stream(validationInvocationAnnotations.toArray(new Annotation[0]))).toArray(Annotation[]::new);
779-
if (ctxAnnotation31 != null) {
780-
ctxAnnotation31 = Stream.concat(Arrays.stream(ctxAnnotation31), Arrays.stream(validationInvocationAnnotations.toArray(new Annotation[0]))).toArray(Annotation[]::new);
771+
if (ctxFilteredSiblingAnnotations != null) {
772+
ctxFilteredSiblingAnnotations = Stream.concat(Arrays.stream(ctxFilteredSiblingAnnotations), Arrays.stream(validationInvocationAnnotations.toArray(new Annotation[0]))).toArray(Annotation[]::new);
781773
}
782774
AnnotatedType aType = new AnnotatedType()
783775
.type(propType)
@@ -790,11 +782,8 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
790782
.components(annotatedType.getComponents())
791783
.propertyName(propName)
792784
.resolveEnumAsRef(AnnotationsUtils.computeEnumAsRef(ctxSchema, ctxArraySchema));
793-
if (
794-
Schema.SchemaResolution.ALL_OF.equals(resolvedSchemaResolution) ||
795-
Schema.SchemaResolution.ALL_OF_REF.equals(resolvedSchemaResolution) ||
796-
openapi31) {
797-
aType.ctxAnnotations(ctxAnnotation31);
785+
if (AnnotationsUtils.areSiblingsAllowed(resolvedSchemaResolution, openapi31)) {
786+
aType.ctxAnnotations(ctxFilteredSiblingAnnotations);
798787
} else {
799788
aType.ctxAnnotations(annotations);
800789
}
@@ -3575,4 +3564,13 @@ private Optional<Schema> resolveArraySchemaWithCycleGuard(
35753564
}
35763565
return reResolvedProperty;
35773566
}
3567+
3568+
/**
3569+
* Checks if the given JavaType represents a java.util.stream.Stream
3570+
*/
3571+
private boolean isStreamType(JavaType type) {
3572+
return type != null &&
3573+
type.getRawClass() != null &&
3574+
java.util.stream.Stream.class.isAssignableFrom(type.getRawClass());
3575+
}
35783576
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package io.swagger.v3.core.util;
2+
3+
import com.fasterxml.jackson.databind.JavaType;
4+
import io.swagger.v3.oas.models.media.Schema;
5+
6+
import java.lang.annotation.Annotation;
7+
import java.util.ArrayList;
8+
import java.util.List;
9+
10+
public class SiblingAnnotationFilter {
11+
12+
public static class FilterResult {
13+
private final Annotation[] filteredAnnotations;
14+
private final Schema.SchemaResolution resolvedSchemaResolution;
15+
16+
public FilterResult(Annotation[] filteredAnnotations, Schema.SchemaResolution resolvedSchemaResolution) {
17+
this.filteredAnnotations = filteredAnnotations;
18+
this.resolvedSchemaResolution = resolvedSchemaResolution;
19+
}
20+
21+
public Annotation[] getFilteredAnnotations() {
22+
return filteredAnnotations;
23+
}
24+
25+
public Schema.SchemaResolution getResolvedSchemaResolution() {
26+
return resolvedSchemaResolution;
27+
}
28+
}
29+
30+
/**
31+
* Filter out Schema/ArraySchema annotations to prevent duplicate processing:
32+
* 1. They are already processed and merged by AnnotationsUtils.mergeSchemaAnnotations()
33+
* 2. Re-processing would cause annotation metadata to leak incorrectly between levels
34+
* 3. Without preserving ArraySchema, Stream is treated as generic object instead of iterable collection.
35+
* * The ArraySchema is needed for the Stream to be recognized as an array, but it doesn't leak to items
36+
* * because the container path filters it out.
37+
*
38+
* @param annotations the annotations to filter
39+
* @param propType the property type
40+
* @param ctxSchema the schema annotation
41+
* @param ctxArraySchema the array schema annotation
42+
* @param schemaResolution the schema resolution configuration
43+
* @param openapi31 whether OpenAPI 3.1 is enabled
44+
* @return FilterResult containing the filtered annotations and resolved schema resolution
45+
*/
46+
public static FilterResult filterSiblingAnnotations(
47+
Annotation[] annotations,
48+
JavaType propType,
49+
io.swagger.v3.oas.annotations.media.Schema ctxSchema,
50+
io.swagger.v3.oas.annotations.media.ArraySchema ctxArraySchema,
51+
Schema.SchemaResolution schemaResolution,
52+
boolean openapi31) {
53+
54+
Annotation[] ctxFilteredSiblingAnnotations = null;
55+
56+
Schema.SchemaResolution resolvedSchemaResolution = AnnotationsUtils.resolveSchemaResolution(schemaResolution, ctxSchema);
57+
58+
if (AnnotationsUtils.areSiblingsAllowed(resolvedSchemaResolution, openapi31)) {
59+
List<Annotation> filteredAnnotationsList = new ArrayList<>();
60+
61+
if (annotations != null) {
62+
boolean isStreamWithArraySchema = isStreamType(propType) && ctxArraySchema != null;
63+
64+
for (Annotation a : annotations) {
65+
boolean isSchemaAnnotation = a instanceof io.swagger.v3.oas.annotations.media.Schema;
66+
boolean isArraySchemaAnnotation = a instanceof io.swagger.v3.oas.annotations.media.ArraySchema;
67+
boolean shouldIncludeAnnotation = (!isSchemaAnnotation && !isArraySchemaAnnotation) || isStreamWithArraySchema;
68+
69+
if (shouldIncludeAnnotation) {
70+
filteredAnnotationsList.add(a);
71+
}
72+
}
73+
74+
ctxFilteredSiblingAnnotations = filteredAnnotationsList.toArray(new Annotation[filteredAnnotationsList.size()]);
75+
}
76+
}
77+
78+
return new FilterResult(ctxFilteredSiblingAnnotations, resolvedSchemaResolution);
79+
}
80+
81+
private static boolean isStreamType(JavaType type) {
82+
return type != null &&
83+
type.getRawClass() != null &&
84+
java.util.stream.Stream.class.isAssignableFrom(type.getRawClass());
85+
}
86+
}

0 commit comments

Comments
 (0)