Skip to content

Commit 3950835

Browse files
Mattias-Sehlstedtewaostrowskadaniel-kmiecik
authored
fix: treat number example as number and not string (#5062)
* fix: treat number example as number and not string * add additional tests showcasing that the example is correctly set based on the field type * add handling of numeric values for examples and default --------- Co-authored-by: Ewa Ostrowska <ewa.ostrowska@smartbear.com> Co-authored-by: Daniel Kmiecik <daniel.kmiecik@smartbear.com>
1 parent 0e21bff commit 3950835

5 files changed

Lines changed: 388 additions & 11 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
@@ -3292,7 +3292,7 @@ protected void resolveSchemaMembers(Schema schema, Annotated a, Annotation[] ann
32923292
schema.setContentMediaType(contentMediaType);
32933293
}
32943294
if (schemaAnnotation.examples().length > 0) {
3295-
List<Object> parsedExamples = io.swagger.v3.core.util.AnnotationsUtils.parseExamplesArray(schemaAnnotation);
3295+
List<Object> parsedExamples = io.swagger.v3.core.util.AnnotationsUtils.parseExamplesArray(schemaAnnotation, schema);
32963296
if (schema.getExamples() == null || schema.getExamples().isEmpty()) {
32973297
schema.setExamples(parsedExamples);
32983298
} else {

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

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ public static boolean hasSchemaAnnotation(io.swagger.v3.oas.annotations.media.Sc
130130
&& StringUtils.isBlank(schema._const())
131131
&& schema.additionalProperties().equals(io.swagger.v3.oas.annotations.media.Schema.AdditionalPropertiesValue.USE_ADDITIONAL_PROPERTIES_ANNOTATION)
132132
&& schema.additionalPropertiesSchema().equals(Void.class)
133+
&& schema.examples().length == 0
133134
) {
134135
return false;
135136
}
@@ -586,7 +587,7 @@ private static void applyArraySchemaAnnotation(io.swagger.v3.oas.annotations.med
586587
arraySchemaObject.setWriteOnly(null);
587588
}
588589
if (openapi31 && arraySchemaAnnotation.examples().length > 0) {
589-
arraySchemaObject.setExamples(parseExamplesArray(arraySchemaAnnotation));
590+
arraySchemaObject.setExamples(parseExamplesArray(arraySchemaAnnotation, arraySchemaObject));
590591
}
591592
}
592593

@@ -777,7 +778,7 @@ public static Optional<Schema> getSchemaFromAnnotation(
777778
schemaObject.setUnevaluatedProperties(resolveSchemaFromType(schema.unevaluatedProperties(), components, jsonViewAnnotation, openapi31, null, null, context));
778779
}
779780
if (openapi31 && schema.examples().length > 0) {
780-
schemaObject.setExamples(parseExamplesArray(schema));
781+
schemaObject.setExamples(parseExamplesArray(schema, schemaObject));
781782
}
782783

783784
if (schema.defaultValue() != null && !DEFAULT_SENTINEL.equals(schema.defaultValue())) {
@@ -917,7 +918,7 @@ private static void setExampleSchema(io.swagger.v3.oas.annotations.media.Schema
917918
// Only parse "null" as null value when nullable=true
918919
if (node.isNull() && schema.nullable()) {
919920
schemaObject.setExample(null);
920-
} else if (node.isObject() || node.isArray()) {
921+
} else if (shouldUseNodeAsExample(node, schemaObject)) {
921922
schemaObject.setExample(node);
922923
} else {
923924
schemaObject.setExample(exampleValue);
@@ -927,6 +928,19 @@ private static void setExampleSchema(io.swagger.v3.oas.annotations.media.Schema
927928
}
928929
}
929930

931+
private static boolean shouldUseNodeAsExample(JsonNode node, Schema schemaObject) {
932+
if (node.isObject() || node.isArray()) {
933+
return true;
934+
}
935+
if (schemaObject != null && SchemaTypeUtils.isNumberSchema(schemaObject)) {
936+
return true;
937+
}
938+
if (schemaObject != null && SchemaTypeUtils.isStringSchema(schemaObject)) {
939+
return false;
940+
}
941+
return node.isNumber();
942+
}
943+
930944
private static void setDefaultSchema(io.swagger.v3.oas.annotations.media.Schema schema, boolean openapi31, Schema schemaObject) {
931945
String defaultValue = schema.defaultValue().trim();
932946
final ObjectMapper mapper = openapi31 ? Json31.mapper() : Json.mapper();
@@ -947,6 +961,10 @@ private static void setDefaultSchema(io.swagger.v3.oas.annotations.media.Schema
947961
}
948962

949963
public static List<Object> parseExamplesArray(io.swagger.v3.oas.annotations.media.Schema schema) {
964+
return parseExamplesArray(schema, null);
965+
}
966+
967+
public static List<Object> parseExamplesArray(io.swagger.v3.oas.annotations.media.Schema schema, Schema schemaObject) {
950968
String[] examplesArray = schema.examples();
951969
List<Object> parsedExamples = new ArrayList<>();
952970
final ObjectMapper mapper = Json31.mapper();
@@ -963,7 +981,9 @@ public static List<Object> parseExamplesArray(io.swagger.v3.oas.annotations.medi
963981
// Only parse "null" as null value when nullable=true
964982
if (node.isNull() && schema.nullable()) {
965983
parsedExamples.add(null);
966-
} else if (node.isObject() || node.isArray()) {
984+
} else if (schemaObject == null && "string".equals(schema.type())) {
985+
parsedExamples.add(trimmed);
986+
} else if (shouldUseNodeAsExample(node, schemaObject)) {
967987
parsedExamples.add(node);
968988
} else {
969989
parsedExamples.add(trimmed);
@@ -983,7 +1003,12 @@ public static Schema resolveSchemaFromType(Class<?> schemaImplementation, Compon
9831003
public static Schema resolveSchemaFromType(Class<?> schemaImplementation, Components components, JsonView jsonViewAnnotation, boolean openapi31) {
9841004
return resolveSchemaFromType(schemaImplementation, components, jsonViewAnnotation, openapi31, null, null, null);
9851005
}
986-
public static Schema resolveSchemaFromType(Class<?> schemaImplementation, Components components, JsonView jsonViewAnnotation, boolean openapi31, io.swagger.v3.oas.annotations.media.Schema schemaAnnotation, io.swagger.v3.oas.annotations.media.ArraySchema arrayAnnotation, ModelConverterContext context) {
1006+
public static Schema resolveSchemaFromType(Class<?> schemaImplementation,
1007+
Components components,
1008+
JsonView jsonViewAnnotation,
1009+
boolean openapi31,
1010+
io.swagger.v3.oas.annotations.media.Schema schemaAnnotation,
1011+
io.swagger.v3.oas.annotations.media.ArraySchema arrayAnnotation, ModelConverterContext context) {
9871012
Schema schemaObject;
9881013
PrimitiveType primitiveType = PrimitiveType.fromType(schemaImplementation);
9891014
if (primitiveType != null) {
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package io.swagger.v3.core.converting;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import io.swagger.v3.core.converter.ModelConverters;
6+
import io.swagger.v3.core.converter.ResolvedSchema;
7+
import io.swagger.v3.core.util.Json;
8+
import io.swagger.v3.core.util.Json31;
9+
import org.testng.annotations.Test;
10+
11+
import java.math.BigDecimal;
12+
13+
import static org.testng.Assert.assertNotNull;
14+
import static org.testng.Assert.assertTrue;
15+
16+
/**
17+
* test documenting how example values for numbers/non-number differ in their JSON-representation.
18+
*/
19+
public class Issue5061Test {
20+
21+
22+
@Test
23+
public void testExampleValuesAreSerializedAsJsonDifferentlyBetweenStringAndNumber() throws Exception {
24+
ResolvedSchema schema = ModelConverters.getInstance(true).readAllAsResolvedSchema(
25+
ModelWithDifferentCombinationOfNumberFieldsWithExamples.class
26+
);
27+
28+
assertNotNull(schema, "Schema should resolve");
29+
String json = Json31.pretty(schema);
30+
assertNotNull(json);
31+
ObjectMapper mapper = new ObjectMapper();
32+
JsonNode root = mapper.readTree(json);
33+
34+
JsonNode stringFieldType = root.at("/schema/properties/stringFieldType");
35+
assertExampleIsString(stringFieldType);
36+
37+
JsonNode stringFieldTypeWithExplicitStringSchemaType = root.at("/schema/properties/stringFieldTypeWithExplicitStringSchemaType");
38+
assertExampleIsString(stringFieldTypeWithExplicitStringSchemaType);
39+
40+
JsonNode stringFieldTypeWithExplicitNumberSchemaType = root.at("/schema/properties/stringFieldTypeWithExplicitNumberSchemaType");
41+
assertExampleIsNumber(stringFieldTypeWithExplicitNumberSchemaType);
42+
43+
JsonNode stringFieldTypeWithExplicitIntegerSchemaType = root.at("/schema/properties/stringFieldTypeWithExplicitIntegerSchemaType");
44+
assertExampleIsNumber(stringFieldTypeWithExplicitIntegerSchemaType);
45+
46+
JsonNode bigDecimalFieldTypeWithExplicitStringSchemaType = root.at("/schema/properties/bigDecimalFieldTypeWithExplicitStringSchemaType");
47+
assertExampleIsString(bigDecimalFieldTypeWithExplicitStringSchemaType);
48+
49+
JsonNode bigDecimalFieldType = root.at("/schema/properties/bigDecimalFieldType");
50+
assertExampleIsNumber(bigDecimalFieldType);
51+
}
52+
53+
@Test
54+
public void testDefaultValueNumericConversionThroughModelResolver() throws Exception {
55+
ResolvedSchema schema = ModelConverters.getInstance(false).readAllAsResolvedSchema(
56+
ModelWithDefaultValues.class
57+
);
58+
59+
assertNotNull(schema, "Schema should resolve");
60+
String json = Json.pretty(schema);
61+
assertNotNull(json);
62+
ObjectMapper mapper = new ObjectMapper();
63+
JsonNode root = mapper.readTree(json);
64+
65+
JsonNode bigDecimalDefault = root.at("/schema/properties/bigDecimalWithDefault");
66+
assertTrue(bigDecimalDefault.get("default").isNumber(),
67+
"default on BigDecimal field should be serialized as a JSON number");
68+
69+
JsonNode stringDefault = root.at("/schema/properties/stringWithDefault");
70+
assertTrue(stringDefault.get("default").isTextual(),
71+
"default on explicit type=\"string\" field should remain a JSON string");
72+
}
73+
74+
@Test
75+
public void testExamplesArrayValuesAreSerializedAsJsonNumbers() throws Exception {
76+
ResolvedSchema schema = ModelConverters.getInstance(true).readAllAsResolvedSchema(
77+
ModelWithExamplesArray.class
78+
);
79+
80+
assertNotNull(schema, "Schema should resolve");
81+
String json = Json31.pretty(schema);
82+
assertNotNull(json);
83+
ObjectMapper mapper = new ObjectMapper();
84+
JsonNode root = mapper.readTree(json);
85+
86+
JsonNode bigDecimalExamples = root.at("/schema/properties/bigDecimalWithExamples");
87+
assertNotNull(bigDecimalExamples.get("examples"), "examples key should exist in JSON, full node: " + bigDecimalExamples);
88+
assertTrue(bigDecimalExamples.get("examples").get(0).isNumber(),
89+
"examples on BigDecimal field should be serialized as JSON numbers");
90+
assertTrue(bigDecimalExamples.get("examples").get(1).isNumber(),
91+
"examples on BigDecimal field should be serialized as JSON numbers");
92+
93+
JsonNode stringExamples = root.at("/schema/properties/stringWithExamples");
94+
assertTrue(stringExamples.get("examples").get(0).isTextual(),
95+
"examples on explicit type=\"string\" field should remain JSON strings");
96+
}
97+
98+
private void assertExampleIsNumber(JsonNode node) {
99+
assertTrue(node.get("example").isNumber(), "should be a number");
100+
}
101+
102+
private void assertExampleIsString(JsonNode node) {
103+
assertTrue(node.get("example").isTextual(), "should be a string");
104+
}
105+
106+
public static class ModelWithDefaultValues {
107+
108+
@io.swagger.v3.oas.annotations.media.Schema(defaultValue = "10.00")
109+
BigDecimal bigDecimalWithDefault;
110+
111+
@io.swagger.v3.oas.annotations.media.Schema(defaultValue = "42", type = "string")
112+
String stringWithDefault;
113+
114+
public BigDecimal getBigDecimalWithDefault() {
115+
return bigDecimalWithDefault;
116+
}
117+
118+
public String getStringWithDefault() {
119+
return stringWithDefault;
120+
}
121+
}
122+
123+
public static class ModelWithExamplesArray {
124+
125+
@io.swagger.v3.oas.annotations.media.Schema(examples = {"10.00", "20.50"})
126+
BigDecimal bigDecimalWithExamples;
127+
128+
@io.swagger.v3.oas.annotations.media.Schema(examples = {"hello", "world"}, type = "string")
129+
String stringWithExamples;
130+
131+
public BigDecimal getBigDecimalWithExamples() {
132+
return bigDecimalWithExamples;
133+
}
134+
135+
public String getStringWithExamples() {
136+
return stringWithExamples;
137+
}
138+
}
139+
140+
public static class ModelWithDifferentCombinationOfNumberFieldsWithExamples {
141+
142+
@io.swagger.v3.oas.annotations.media.Schema(example = "5 lacs per annum")
143+
String stringFieldType;
144+
145+
@io.swagger.v3.oas.annotations.media.Schema(type = "string", example = "5 lacs per annum")
146+
String stringFieldTypeWithExplicitStringSchemaType;
147+
148+
@io.swagger.v3.oas.annotations.media.Schema(type = "number", example = "10")
149+
String stringFieldTypeWithExplicitNumberSchemaType;
150+
151+
@io.swagger.v3.oas.annotations.media.Schema(type = "integer", example = "5")
152+
String stringFieldTypeWithExplicitIntegerSchemaType;
153+
154+
@io.swagger.v3.oas.annotations.media.Schema(type = "string", example = "13.37")
155+
BigDecimal bigDecimalFieldTypeWithExplicitStringSchemaType;
156+
157+
@io.swagger.v3.oas.annotations.media.Schema(example = "13.37")
158+
BigDecimal bigDecimalFieldType;
159+
160+
public String getStringFieldType() {
161+
return stringFieldType;
162+
}
163+
164+
public String getStringFieldTypeWithExplicitStringSchemaType() {
165+
return stringFieldTypeWithExplicitStringSchemaType;
166+
}
167+
168+
public String getStringFieldTypeWithExplicitNumberSchemaType() {
169+
return stringFieldTypeWithExplicitNumberSchemaType;
170+
}
171+
172+
public String getStringFieldTypeWithExplicitIntegerSchemaType() {
173+
return stringFieldTypeWithExplicitIntegerSchemaType;
174+
}
175+
176+
public BigDecimal getBigDecimalFieldTypeWithExplicitStringSchemaType() {
177+
return bigDecimalFieldTypeWithExplicitStringSchemaType;
178+
}
179+
180+
public BigDecimal getBigDecimalFieldType() {
181+
return bigDecimalFieldType;
182+
}
183+
184+
}
185+
}

modules/swagger-core/src/test/java/io/swagger/v3/core/issues/Issue4339Test.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.swagger.v3.core.issues;
22

3+
import com.fasterxml.jackson.databind.node.IntNode;
4+
import com.fasterxml.jackson.databind.node.NullNode;
35
import io.swagger.v3.core.converter.AnnotatedType;
46
import io.swagger.v3.core.converter.ModelConverterContextImpl;
57
import io.swagger.v3.core.jackson.ModelResolver;
@@ -514,8 +516,8 @@ public void testNonNullableIntegerWithNullExampleAndDefault_OAS31() {
514516
(io.swagger.v3.oas.models.media.Schema) model.getProperties().get("integerField");
515517
assertNotNull(integerField, "integerField property should exist");
516518

517-
assertEquals(integerField.getExample(), "null",
518-
"Example should be the string \"null\", not null value");
519+
assertEquals(integerField.getExample(), NullNode.getInstance(),
520+
"Example should be the NullNode \"null\", not null value");
519521
assertEquals(integerField.getDefault(), "null",
520522
"Default should be the string \"null\", not null value");
521523
}
@@ -610,9 +612,9 @@ public void testNullableIntegerWithMultipleExamplesIncludingNull_OAS31() {
610612
assertNotNull(integerField.getExamples(), "examples array should exist");
611613
assertEquals(integerField.getExamples().size(), 3, "examples array should have 3 elements");
612614

613-
assertTrue(integerField.getExamples().contains("1"), "examples should contain '1' as string");
615+
assertTrue(integerField.getExamples().contains(IntNode.valueOf(1)), "examples should contain 1 as number");
614616
assertTrue(integerField.getExamples().contains(null), "examples should contain null value");
615-
assertTrue(integerField.getExamples().contains("100"), "examples should contain '100' as string");
617+
assertTrue(integerField.getExamples().contains(IntNode.valueOf(100)), "examples should contain 100 as number");
616618
}
617619

618620
/**

0 commit comments

Comments
 (0)