Skip to content

Commit dd9a512

Browse files
fix: correct sibling logic when reading type from schema annotation
1 parent f3de17d commit dd9a512

10 files changed

Lines changed: 284 additions & 56 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -798,7 +798,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context
798798
});
799799

800800
boolean areSiblingsAllowed = AnnotationsUtils.areSiblingsAllowed(resolvedSchemaResolution, openapi31);
801-
aType = AnnotationsUtils.addTypeWhenSiblingsAllowed(aType, ctxSchema, areSiblingsAllowed);
801+
aType = AnnotationsUtils.addTypeWhenSiblingsAllowed(aType, ctxSchema, openapi31, areSiblingsAllowed);
802802
property = context.resolve(aType);
803803
property = clone(property);
804804
Schema ctxProperty = null;

modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@
6161
public abstract class AnnotationsUtils {
6262

6363
private static final Logger LOGGER = LoggerFactory.getLogger(AnnotationsUtils.class);
64+
private static final String NULL_TYPE = "null";
65+
private static final String STRING_TYPE = "string";
6466
public static final String COMPONENTS_REF = Components.COMPONENTS_SCHEMAS_REF;
6567

6668
public static boolean hasSchemaAnnotation(io.swagger.v3.oas.annotations.media.Schema schema) {
@@ -992,7 +994,7 @@ public static List<Object> parseExamplesArray(io.swagger.v3.oas.annotations.medi
992994
// Only parse "null" as null value when nullable=true
993995
if (node.isNull() && schema.nullable()) {
994996
parsedExamples.add(null);
995-
} else if (schemaObject == null && "string".equals(schema.type())) {
997+
} else if (schemaObject == null && STRING_TYPE.equals(schema.type())) {
996998
parsedExamples.add(trimmed);
997999
} else if (shouldUseNodeAsExample(node, schemaObject)) {
9981000
parsedExamples.add(node);
@@ -1070,7 +1072,7 @@ public static Schema resolveSchemaFromType(Class<?> schemaImplementation,
10701072
}
10711073
if (StringUtils.isBlank(existingSchemaObject.get$ref()) && StringUtils.isBlank(existingSchemaObject.getType())) {
10721074
// default to string
1073-
existingSchemaObject.setType("string");
1075+
existingSchemaObject.setType(STRING_TYPE);
10741076
}
10751077
return existingSchemaObject;
10761078
}
@@ -1700,7 +1702,7 @@ public static Type getSchemaType(io.swagger.v3.oas.annotations.media.Schema sche
17001702
}
17011703
case "boolean":
17021704
return Boolean.class;
1703-
case "string":
1705+
case STRING_TYPE:
17041706
return String.class;
17051707
default:
17061708
if (nullIfNotFound) {
@@ -1935,26 +1937,28 @@ public static Optional<? extends Schema> getSchema(io.swagger.v3.oas.annotations
19351937
} else {
19361938
Optional<Schema> schemaFromAnnotation = AnnotationsUtils.getSchemaFromAnnotation(schemaAnnotation, components, jsonViewAnnotation, openapi31, null, context);
19371939
if (schemaFromAnnotation.isPresent()) {
1938-
if (StringUtils.isBlank(schemaFromAnnotation.get().get$ref()) && StringUtils.isBlank(schemaFromAnnotation.get().getType()) && !(schemaFromAnnotation.get() instanceof ComposedSchema)) {
1940+
Schema schema = schemaFromAnnotation.get();
1941+
if (StringUtils.isBlank(schema.get$ref()) && StringUtils.isBlank(schema.getType()) && !(schema instanceof ComposedSchema)) {
19391942
// default to string
1940-
schemaFromAnnotation.get().setType("string");
1943+
schema.setType(STRING_TYPE);
19411944
}
1942-
return Optional.of(schemaFromAnnotation.get());
1945+
return Optional.of(schema);
19431946
} else {
19441947
Optional<Schema> arraySchemaFromAnnotation = AnnotationsUtils.getArraySchema(arrayAnnotation, components, jsonViewAnnotation, openapi31, null, false, context);
19451948
if (arraySchemaFromAnnotation.isPresent()) {
1946-
if (arraySchemaFromAnnotation.get().getItems() != null && StringUtils.isBlank(arraySchemaFromAnnotation.get().getItems().get$ref()) && StringUtils.isBlank(arraySchemaFromAnnotation.get().getItems().getType())) {
1949+
Schema schema = arraySchemaFromAnnotation.get();
1950+
Schema schemaItems = schema.getItems();
1951+
if (schemaItems != null && StringUtils.isBlank(schemaItems.get$ref()) && StringUtils.isBlank(schemaItems.getType())) {
19471952
// default to string
1948-
arraySchemaFromAnnotation.get().getItems().setType("string");
1953+
schemaItems.setType(STRING_TYPE);
19491954
}
1950-
return Optional.of(arraySchemaFromAnnotation.get());
1955+
return Optional.of(schema);
19511956
}
19521957
}
19531958
}
19541959
return Optional.empty();
19551960
}
19561961

1957-
19581962
public static void applyTypes(String[] classTypes, String[] methodTypes, Content content, MediaType mediaType) {
19591963
if (methodTypes != null && methodTypes.length > 0) {
19601964
for (String value : methodTypes) {
@@ -3076,14 +3080,32 @@ public static boolean areSiblingsAllowed(Schema.SchemaResolution resolvedSchemaR
30763080
return Schema.SchemaResolution.ALL_OF.equals(resolvedSchemaResolution) || Schema.SchemaResolution.ALL_OF_REF.equals(resolvedSchemaResolution) || openapi31;
30773081
}
30783082

3079-
public static AnnotatedType addTypeWhenSiblingsAllowed(AnnotatedType aType, io.swagger.v3.oas.annotations.media.Schema ctxSchema, boolean areSiblingsAllowed) {
3083+
public static AnnotatedType addTypeWhenSiblingsAllowed(AnnotatedType aType,
3084+
io.swagger.v3.oas.annotations.media.Schema ctxSchema,
3085+
boolean openapi31,
3086+
boolean areSiblingsAllowed) {
30803087
if (areSiblingsAllowed && ctxSchema != null) {
30813088
if (!Void.class.equals(ctxSchema.implementation())) {
30823089
aType.setType(ctxSchema.implementation());
3083-
} else if (StringUtils.isNotBlank(ctxSchema.type())) {
3084-
aType.setType(ctxSchema.type().getClass());
3090+
} else if (openapi31) {
3091+
if (isOas31SingleTypeIncludingNull(ctxSchema) && Arrays.asList(ctxSchema.types()).contains(STRING_TYPE)) {
3092+
aType.setType(String.class);
3093+
}
3094+
} else if(StringUtils.isNotBlank(ctxSchema.type()) && STRING_TYPE.equals(ctxSchema.type())) {
3095+
aType.setType(String.class);
30853096
}
30863097
}
30873098
return aType;
30883099
}
3100+
3101+
/**
3102+
* Returns whether the schema annotation defines a single type in {@code types}.
3103+
* Two types where one of them is {@code null} is counted as one type to support defining nullability
3104+
* @param schema the schema annotation
3105+
* @return whether the annotation defines a single type
3106+
*/
3107+
public static boolean isOas31SingleTypeIncludingNull(io.swagger.v3.oas.annotations.media.Schema schema) {
3108+
String[] types = schema.types();
3109+
return types.length == 1 || types.length == 2 && Arrays.asList(types).contains(NULL_TYPE);
3110+
}
30893111
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,16 +142,16 @@ public static class ModelWithDifferentCombinationOfNumberFieldsWithExamples {
142142
@io.swagger.v3.oas.annotations.media.Schema(example = "5 lacs per annum")
143143
String stringFieldType;
144144

145-
@io.swagger.v3.oas.annotations.media.Schema(type = "string", example = "5 lacs per annum")
145+
@io.swagger.v3.oas.annotations.media.Schema(types = {"string"}, example = "5 lacs per annum")
146146
String stringFieldTypeWithExplicitStringSchemaType;
147147

148-
@io.swagger.v3.oas.annotations.media.Schema(type = "number", example = "10")
148+
@io.swagger.v3.oas.annotations.media.Schema(types = {"number"}, example = "10")
149149
String stringFieldTypeWithExplicitNumberSchemaType;
150150

151-
@io.swagger.v3.oas.annotations.media.Schema(type = "integer", example = "5")
151+
@io.swagger.v3.oas.annotations.media.Schema(types = {"integer"}, example = "5")
152152
String stringFieldTypeWithExplicitIntegerSchemaType;
153153

154-
@io.swagger.v3.oas.annotations.media.Schema(type = "string", example = "13.37")
154+
@io.swagger.v3.oas.annotations.media.Schema(types = {"string"}, example = "13.37")
155155
BigDecimal bigDecimalFieldTypeWithExplicitStringSchemaType;
156156

157157
@io.swagger.v3.oas.annotations.media.Schema(example = "13.37")

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,10 @@ public static class ModelWithDifferentCombinationOfBooleanFieldsWithExamples {
5555
@io.swagger.v3.oas.annotations.media.Schema(example = "true")
5656
String stringFieldType;
5757

58-
@io.swagger.v3.oas.annotations.media.Schema(type = "boolean", example = "true")
58+
@io.swagger.v3.oas.annotations.media.Schema(types = {"boolean"}, example = "true")
5959
String stringFieldTypeWithExplicitBooleanSchemaType;
6060

61-
@io.swagger.v3.oas.annotations.media.Schema(type = "string", example = "true")
61+
@io.swagger.v3.oas.annotations.media.Schema(types = {"string"}, example = "true")
6262
boolean booleanFieldTypeWithExplicitStringSchemaType;
6363

6464
@io.swagger.v3.oas.annotations.media.Schema(example = "true")
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package io.swagger.v3.core.resolving;
2+
3+
import io.swagger.v3.core.converter.ModelConverters;
4+
import io.swagger.v3.core.matchers.SerializationMatchers;
5+
import io.swagger.v3.oas.models.media.Schema;
6+
import org.testng.annotations.Test;
7+
8+
import java.math.BigDecimal;
9+
import java.util.Map;
10+
11+
public class SchemaAnnotationSetsTypeTest {
12+
13+
@Test(description = "Setting type in @Schema is applied for OAS 3.0")
14+
public void testTypeInSchemaAnnotationIsAppliedForOAS30() {
15+
String expectedYaml = "ClassWithFieldsUsingOAS30Type:\n" +
16+
" type: object\n" +
17+
" properties:\n" +
18+
" inferred:\n" +
19+
" type: number\n" +
20+
" amount:\n" +
21+
" type: boolean\n" +
22+
" count:\n" +
23+
" type: boolean\n" +
24+
" flag:\n" +
25+
" type: integer\n" +
26+
" format: int32\n" +
27+
" unit:\n" +
28+
" type: string\n" +
29+
" enum:\n" +
30+
" - DAY\n" +
31+
" - WEEK\n" +
32+
" - MONTH";
33+
34+
Map<String, Schema> stringSchemaMap = ModelConverters.getInstance().readAll(ClassWithFieldsUsingOAS30Type.class);
35+
SerializationMatchers.assertEqualsToYaml(stringSchemaMap, expectedYaml);
36+
}
37+
38+
@Test(description = "Setting types in @Schema is ignored for OAS 3.0")
39+
public void testTypesInSchemaAnnotationIsIgnoredForOAS30() {
40+
String expectedYaml = "ClassWithFieldsUsingOAS31Types:\n" +
41+
" type: object\n" +
42+
" properties:\n" +
43+
" inferred:\n" +
44+
" type: number\n" +
45+
" amount:\n" +
46+
" type: number\n" +
47+
" count:\n" +
48+
" type: integer\n" +
49+
" format: int32\n" +
50+
" flag:\n" +
51+
" type: boolean\n" +
52+
" unit:\n" +
53+
" type: string\n" +
54+
" enum:\n" +
55+
" - DAY\n" +
56+
" - WEEK\n" +
57+
" - MONTH";
58+
59+
Map<String, io.swagger.v3.oas.models.media.Schema> stringSchemaMap = ModelConverters.getInstance().readAll(ClassWithFieldsUsingOAS31Types.class);
60+
SerializationMatchers.assertEqualsToYaml(stringSchemaMap, expectedYaml);
61+
}
62+
63+
@Test(description = "Setting type in @Schema is ignored for OAS 3.1")
64+
public void testTypeInSchemaAnnotationIsIgnoredForOAS31() {
65+
String expectedYaml = "ClassWithFieldsUsingOAS30Type:\n" +
66+
" type: object\n" +
67+
" properties:\n" +
68+
" inferred:\n" +
69+
" type: number\n" +
70+
" amount:\n" +
71+
" type: number\n" +
72+
" count:\n" +
73+
" type: integer\n" +
74+
" format: int32\n" +
75+
" flag:\n" +
76+
" type: boolean\n" +
77+
" unit:\n" +
78+
" type: string\n" +
79+
" enum:\n" +
80+
" - DAY\n" +
81+
" - WEEK\n" +
82+
" - MONTH";
83+
84+
Map<String, io.swagger.v3.oas.models.media.Schema> stringSchemaMap = ModelConverters.getInstance(true)
85+
.readAll(ClassWithFieldsUsingOAS30Type.class);
86+
SerializationMatchers.assertEqualsToYaml31(stringSchemaMap, expectedYaml);
87+
}
88+
89+
@Test(description = "Setting types in @Schema is applied for OAS 3.1")
90+
public void testTypesInSchemaAnnotationIsAppliedForOAS31() {
91+
String expectedYaml = "ClassWithFieldsUsingOAS31Types:\n" +
92+
" type: object\n" +
93+
" properties:\n" +
94+
" inferred:\n" +
95+
" type: number\n" +
96+
" amount:\n" +
97+
" type: boolean\n" +
98+
" count:\n" +
99+
" type: boolean\n" +
100+
" format: int32\n" +
101+
" flag:\n" +
102+
" type: integer\n" +
103+
" unit:\n" +
104+
" type: string\n" +
105+
" enum:\n" +
106+
" - DAY\n" +
107+
" - WEEK\n" +
108+
" - MONTH";
109+
110+
Map<String, io.swagger.v3.oas.models.media.Schema> stringSchemaMap = ModelConverters.getInstance(true)
111+
.readAll(ClassWithFieldsUsingOAS31Types.class);
112+
SerializationMatchers.assertEqualsToYaml31(stringSchemaMap, expectedYaml);
113+
}
114+
115+
@Test(description = "Setting several types in @Schema is applied for OAS 3.1")
116+
public void testSeveralTypesInSchemaAnnotationIsAppliedForOAS31() {
117+
String expectedYaml = "ClassWithFieldsUsingOAS31TypesDefiningSeveralTypes:\n" +
118+
" type: object\n" +
119+
" properties:\n" +
120+
" inferred:\n" +
121+
" type: number\n" +
122+
" amount:\n" +
123+
" type:\n" +
124+
" - boolean\n" +
125+
" - number\n" +
126+
" count:\n" +
127+
" type:\n" +
128+
" - boolean\n" +
129+
" - number\n" +
130+
" format: int32\n" +
131+
" flag:\n" +
132+
" type:\n" +
133+
" - integer\n" +
134+
" - number\n" +
135+
" unit:\n" +
136+
" type: string\n" +
137+
" enum:\n" +
138+
" - DAY\n" +
139+
" - WEEK\n" +
140+
" - MONTH";
141+
142+
Map<String, io.swagger.v3.oas.models.media.Schema> stringSchemaMap = ModelConverters.getInstance(true)
143+
.readAll(ClassWithFieldsUsingOAS31TypesDefiningSeveralTypes.class);
144+
SerializationMatchers.assertEqualsToYaml31(stringSchemaMap, expectedYaml);
145+
}
146+
147+
private static class ClassWithFieldsUsingOAS30Type {
148+
@io.swagger.v3.oas.annotations.media.Schema
149+
public BigDecimal inferred;
150+
@io.swagger.v3.oas.annotations.media.Schema(type = "boolean")
151+
public BigDecimal amount;
152+
@io.swagger.v3.oas.annotations.media.Schema(type = "boolean")
153+
public Integer count;
154+
@io.swagger.v3.oas.annotations.media.Schema(type = "integer")
155+
public Boolean flag;
156+
@io.swagger.v3.oas.annotations.media.Schema
157+
public Frequency unit;
158+
159+
enum Frequency { DAY, WEEK, MONTH }
160+
}
161+
162+
private static class ClassWithFieldsUsingOAS31Types {
163+
@io.swagger.v3.oas.annotations.media.Schema
164+
public BigDecimal inferred;
165+
@io.swagger.v3.oas.annotations.media.Schema(types = {"boolean"})
166+
public BigDecimal amount;
167+
@io.swagger.v3.oas.annotations.media.Schema(types = {"boolean"})
168+
public Integer count;
169+
@io.swagger.v3.oas.annotations.media.Schema(types = {"integer"})
170+
public Boolean flag;
171+
@io.swagger.v3.oas.annotations.media.Schema
172+
public Frequency unit;
173+
174+
enum Frequency { DAY, WEEK, MONTH }
175+
}
176+
177+
private static class ClassWithFieldsUsingOAS31TypesDefiningSeveralTypes {
178+
@io.swagger.v3.oas.annotations.media.Schema
179+
public BigDecimal inferred;
180+
@io.swagger.v3.oas.annotations.media.Schema(types = {"boolean", "number"})
181+
public BigDecimal amount;
182+
@io.swagger.v3.oas.annotations.media.Schema(types = {"boolean", "number"})
183+
public Integer count;
184+
@io.swagger.v3.oas.annotations.media.Schema(types = {"integer", "number"})
185+
public Boolean flag;
186+
@io.swagger.v3.oas.annotations.media.Schema
187+
public Frequency unit;
188+
189+
enum Frequency { DAY, WEEK, MONTH }
190+
}
191+
192+
}

0 commit comments

Comments
 (0)