From 21d768283c154f8bdc193cdeb3cb6e400c76a716 Mon Sep 17 00:00:00 2001 From: Mattias-Sehlstedt <60173714+Mattias-Sehlstedt@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:18:51 +0100 Subject: [PATCH 1/3] fix: treat number example as number and not string --- .../v3/core/util/AnnotationsUtils.java | 9 +++- .../v3/core/util/AnnotationsUtilsTest.java | 51 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java index 2a42ebb37e..a641458092 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java @@ -917,7 +917,7 @@ private static void setExampleSchema(io.swagger.v3.oas.annotations.media.Schema // Only parse "null" as null value when nullable=true if (node.isNull() && schema.nullable()) { schemaObject.setExample(null); - } else if (node.isObject() || node.isArray()) { + } else if (node.isObject() || node.isArray() || SchemaTypeUtils.isNumberSchema(schemaObject)) { schemaObject.setExample(node); } else { schemaObject.setExample(exampleValue); @@ -983,7 +983,12 @@ public static Schema resolveSchemaFromType(Class schemaImplementation, Compon public static Schema resolveSchemaFromType(Class schemaImplementation, Components components, JsonView jsonViewAnnotation, boolean openapi31) { return resolveSchemaFromType(schemaImplementation, components, jsonViewAnnotation, openapi31, null, null, null); } - 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) { + 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) { Schema schemaObject; PrimitiveType primitiveType = PrimitiveType.fromType(schemaImplementation); if (primitiveType != null) { diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java index 2d563cf786..9aff34b371 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java @@ -1,5 +1,6 @@ package io.swagger.v3.core.util; +import com.fasterxml.jackson.databind.node.IntNode; import com.google.common.collect.ImmutableMap; import io.swagger.v3.oas.annotations.ExternalDocumentation; import io.swagger.v3.oas.annotations.Parameter; @@ -109,6 +110,12 @@ class DummyClass implements Serializable {} static class ExampleHolder { @io.swagger.v3.oas.annotations.media.Schema(type = "string", example = "5 lacs per annum") String value; + + @io.swagger.v3.oas.annotations.media.Schema(type = "number", example = "10") + String numberValue; + + @io.swagger.v3.oas.annotations.media.Schema(type = "integer", example = "5") + String integerValue; } @Test @@ -715,4 +722,48 @@ public void sentinelShouldNeverAppearInResolvedSchema() throws Exception { "Sentinel value must never appear in resolved schema default"); } + @Test + public void testExampleWithNumberTypeShouldHaveExampleAsNumber() throws Exception { + io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = + ExampleHolder.class + .getDeclaredField("numberValue") + .getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + + Optional schema = + AnnotationsUtils.getSchemaFromAnnotation( + schemaAnnotation, + null, + null, + false, + null, + Schema.SchemaResolution.DEFAULT, + null + ); + + assertTrue(schema.isPresent()); + assertEquals(schema.get().getExample(), IntNode.valueOf(10)); + } + + @Test + public void testExampleWithIntegerTypeShouldHaveExampleAsNumber() throws Exception { + io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = + ExampleHolder.class + .getDeclaredField("integerValue") + .getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + + Optional schema = + AnnotationsUtils.getSchemaFromAnnotation( + schemaAnnotation, + null, + null, + false, + null, + Schema.SchemaResolution.DEFAULT, + null + ); + + assertTrue(schema.isPresent()); + assertEquals(schema.get().getExample(), IntNode.valueOf(5)); + } + } From 854e452ab079b3e41f8c1975b2240a5261865d03 Mon Sep 17 00:00:00 2001 From: Mattias-Sehlstedt <60173714+Mattias-Sehlstedt@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:24:06 +0100 Subject: [PATCH 2/3] add additional tests showcasing that the example is correctly set based on the field type --- .../v3/core/converting/Issue5061Test.java | 105 ++++++++++++++++++ .../swagger/v3/core/issues/Issue4339Test.java | 5 +- .../v3/core/util/AnnotationsUtilsTest.java | 88 +++++++-------- 3 files changed, 152 insertions(+), 46 deletions(-) create mode 100644 modules/swagger-core/src/test/java/io/swagger/v3/core/converting/Issue5061Test.java diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/Issue5061Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/Issue5061Test.java new file mode 100644 index 0000000000..174b88364d --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/Issue5061Test.java @@ -0,0 +1,105 @@ +package io.swagger.v3.core.converting; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.converter.ResolvedSchema; +import io.swagger.v3.core.util.Json31; +import org.testng.annotations.Test; + +import java.math.BigDecimal; + +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * test documenting how example values for numbers/non-number differ in their JSON-representation. + */ +public class Issue5061Test { + + + @Test + public void testExampleValuesAreSerializedAsJsonDifferentlyBetweenStringAndNumber() throws Exception { + ResolvedSchema schema = ModelConverters.getInstance(true).readAllAsResolvedSchema( + ModelWithDifferentCombinationOfNumberFieldsWithExamples.class + ); + + assertNotNull(schema, "Schema should resolve"); + String json = Json31.pretty(schema); + assertNotNull(json); + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(json); + + JsonNode stringFieldType = root.at("/schema/properties/stringFieldType"); + assertExampleIsString(stringFieldType); + + JsonNode stringFieldTypeWithExplicitStringSchemaType = root.at("/schema/properties/stringFieldTypeWithExplicitStringSchemaType"); + assertExampleIsString(stringFieldTypeWithExplicitStringSchemaType); + + JsonNode stringFieldTypeWithExplicitNumberSchemaType = root.at("/schema/properties/stringFieldTypeWithExplicitNumberSchemaType"); + assertExampleIsNumber(stringFieldTypeWithExplicitNumberSchemaType); + + JsonNode stringFieldTypeWithExplicitIntegerSchemaType = root.at("/schema/properties/stringFieldTypeWithExplicitIntegerSchemaType"); + assertExampleIsNumber(stringFieldTypeWithExplicitIntegerSchemaType); + + JsonNode bigDecimalFieldTypeWithExplicitStringSchemaType = root.at("/schema/properties/bigDecimalFieldTypeWithExplicitStringSchemaType"); + assertExampleIsString(bigDecimalFieldTypeWithExplicitStringSchemaType); + + JsonNode bigDecimalFieldType = root.at("/schema/properties/bigDecimalFieldType"); + assertExampleIsNumber(bigDecimalFieldType); + } + + private void assertExampleIsNumber(JsonNode node) { + assertTrue(node.get("example").isNumber(), "should be a number"); + } + + private void assertExampleIsString(JsonNode node) { + assertTrue(node.get("example").isTextual(), "should be a string"); + } + + public static class ModelWithDifferentCombinationOfNumberFieldsWithExamples { + + @io.swagger.v3.oas.annotations.media.Schema(example = "5 lacs per annum") + String stringFieldType; + + @io.swagger.v3.oas.annotations.media.Schema(type = "string", example = "5 lacs per annum") + String stringFieldTypeWithExplicitStringSchemaType; + + @io.swagger.v3.oas.annotations.media.Schema(type = "number", example = "10") + String stringFieldTypeWithExplicitNumberSchemaType; + + @io.swagger.v3.oas.annotations.media.Schema(type = "integer", example = "5") + String stringFieldTypeWithExplicitIntegerSchemaType; + + @io.swagger.v3.oas.annotations.media.Schema(type = "string", example = "13.37") + BigDecimal bigDecimalFieldTypeWithExplicitStringSchemaType; + + @io.swagger.v3.oas.annotations.media.Schema(example = "13.37") + BigDecimal bigDecimalFieldType; + + public String getStringFieldType() { + return stringFieldType; + } + + public String getStringFieldTypeWithExplicitStringSchemaType() { + return stringFieldTypeWithExplicitStringSchemaType; + } + + public String getStringFieldTypeWithExplicitNumberSchemaType() { + return stringFieldTypeWithExplicitNumberSchemaType; + } + + public String getStringFieldTypeWithExplicitIntegerSchemaType() { + return stringFieldTypeWithExplicitIntegerSchemaType; + } + + public BigDecimal getBigDecimalFieldTypeWithExplicitStringSchemaType() { + return bigDecimalFieldTypeWithExplicitStringSchemaType; + } + + public BigDecimal getBigDecimalFieldType() { + return bigDecimalFieldType; + } + + } +} diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/issues/Issue4339Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/issues/Issue4339Test.java index 59ba8cb176..a45473fd63 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/issues/Issue4339Test.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/issues/Issue4339Test.java @@ -1,5 +1,6 @@ package io.swagger.v3.core.issues; +import com.fasterxml.jackson.databind.node.NullNode; import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.ModelConverterContextImpl; import io.swagger.v3.core.jackson.ModelResolver; @@ -514,8 +515,8 @@ public void testNonNullableIntegerWithNullExampleAndDefault_OAS31() { (io.swagger.v3.oas.models.media.Schema) model.getProperties().get("integerField"); assertNotNull(integerField, "integerField property should exist"); - assertEquals(integerField.getExample(), "null", - "Example should be the string \"null\", not null value"); + assertEquals(integerField.getExample(), NullNode.getInstance(), + "Example should be the NullNode \"null\", not null value"); assertEquals(integerField.getDefault(), "null", "Default should be the string \"null\", not null value"); } diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java index 9aff34b371..8c34b51625 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java @@ -140,6 +140,50 @@ public void testExampleStartingWithNumberShouldBeString() throws Exception { assertEquals(schema.get().getExample(), "5 lacs per annum"); } + @Test + public void testExampleWithNumberTypeShouldHaveExampleAsNumber() throws Exception { + io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = + ExampleHolder.class + .getDeclaredField("numberValue") + .getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + + Optional schema = + AnnotationsUtils.getSchemaFromAnnotation( + schemaAnnotation, + null, + null, + false, + null, + Schema.SchemaResolution.DEFAULT, + null + ); + + assertTrue(schema.isPresent()); + assertEquals(schema.get().getExample(), IntNode.valueOf(10)); + } + + @Test + public void testExampleWithIntegerTypeShouldHaveExampleAsNumber() throws Exception { + io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = + ExampleHolder.class + .getDeclaredField("integerValue") + .getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + + Optional schema = + AnnotationsUtils.getSchemaFromAnnotation( + schemaAnnotation, + null, + null, + false, + null, + Schema.SchemaResolution.DEFAULT, + null + ); + + assertTrue(schema.isPresent()); + assertEquals(schema.get().getExample(), IntNode.valueOf(5)); + } + static class DefaultHolder { @io.swagger.v3.oas.annotations.media.Schema(type = "string") @@ -722,48 +766,4 @@ public void sentinelShouldNeverAppearInResolvedSchema() throws Exception { "Sentinel value must never appear in resolved schema default"); } - @Test - public void testExampleWithNumberTypeShouldHaveExampleAsNumber() throws Exception { - io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = - ExampleHolder.class - .getDeclaredField("numberValue") - .getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); - - Optional schema = - AnnotationsUtils.getSchemaFromAnnotation( - schemaAnnotation, - null, - null, - false, - null, - Schema.SchemaResolution.DEFAULT, - null - ); - - assertTrue(schema.isPresent()); - assertEquals(schema.get().getExample(), IntNode.valueOf(10)); - } - - @Test - public void testExampleWithIntegerTypeShouldHaveExampleAsNumber() throws Exception { - io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = - ExampleHolder.class - .getDeclaredField("integerValue") - .getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); - - Optional schema = - AnnotationsUtils.getSchemaFromAnnotation( - schemaAnnotation, - null, - null, - false, - null, - Schema.SchemaResolution.DEFAULT, - null - ); - - assertTrue(schema.isPresent()); - assertEquals(schema.get().getExample(), IntNode.valueOf(5)); - } - } From 12f88563df015d49998c61925d48d3aad2b1aea4 Mon Sep 17 00:00:00 2001 From: Ewa Ostrowska Date: Fri, 3 Apr 2026 15:47:17 +0200 Subject: [PATCH 3/3] add handling of numeric values for examples and default --- .../v3/core/jackson/ModelResolver.java | 2 +- .../v3/core/util/AnnotationsUtils.java | 28 ++++- .../v3/core/converting/Issue5061Test.java | 80 ++++++++++++ .../swagger/v3/core/issues/Issue4339Test.java | 5 +- .../v3/core/util/AnnotationsUtilsTest.java | 116 +++++++++++++++++- 5 files changed, 223 insertions(+), 8 deletions(-) diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java index e0b91eb592..aa44b8d1e0 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/jackson/ModelResolver.java @@ -3287,7 +3287,7 @@ protected void resolveSchemaMembers(Schema schema, Annotated a, Annotation[] ann schema.setContentMediaType(contentMediaType); } if (schemaAnnotation.examples().length > 0) { - List parsedExamples = io.swagger.v3.core.util.AnnotationsUtils.parseExamplesArray(schemaAnnotation); + List parsedExamples = io.swagger.v3.core.util.AnnotationsUtils.parseExamplesArray(schemaAnnotation, schema); if (schema.getExamples() == null || schema.getExamples().isEmpty()) { schema.setExamples(parsedExamples); } else { diff --git a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java index a641458092..bab6c94269 100644 --- a/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java +++ b/modules/swagger-core/src/main/java/io/swagger/v3/core/util/AnnotationsUtils.java @@ -130,6 +130,7 @@ public static boolean hasSchemaAnnotation(io.swagger.v3.oas.annotations.media.Sc && StringUtils.isBlank(schema._const()) && schema.additionalProperties().equals(io.swagger.v3.oas.annotations.media.Schema.AdditionalPropertiesValue.USE_ADDITIONAL_PROPERTIES_ANNOTATION) && schema.additionalPropertiesSchema().equals(Void.class) + && schema.examples().length == 0 ) { return false; } @@ -586,7 +587,7 @@ private static void applyArraySchemaAnnotation(io.swagger.v3.oas.annotations.med arraySchemaObject.setWriteOnly(null); } if (openapi31 && arraySchemaAnnotation.examples().length > 0) { - arraySchemaObject.setExamples(parseExamplesArray(arraySchemaAnnotation)); + arraySchemaObject.setExamples(parseExamplesArray(arraySchemaAnnotation, arraySchemaObject)); } } @@ -777,7 +778,7 @@ public static Optional getSchemaFromAnnotation( schemaObject.setUnevaluatedProperties(resolveSchemaFromType(schema.unevaluatedProperties(), components, jsonViewAnnotation, openapi31, null, null, context)); } if (openapi31 && schema.examples().length > 0) { - schemaObject.setExamples(parseExamplesArray(schema)); + schemaObject.setExamples(parseExamplesArray(schema, schemaObject)); } if (schema.defaultValue() != null && !DEFAULT_SENTINEL.equals(schema.defaultValue())) { @@ -917,7 +918,7 @@ private static void setExampleSchema(io.swagger.v3.oas.annotations.media.Schema // Only parse "null" as null value when nullable=true if (node.isNull() && schema.nullable()) { schemaObject.setExample(null); - } else if (node.isObject() || node.isArray() || SchemaTypeUtils.isNumberSchema(schemaObject)) { + } else if (shouldUseNodeAsExample(node, schemaObject)) { schemaObject.setExample(node); } else { schemaObject.setExample(exampleValue); @@ -927,6 +928,19 @@ private static void setExampleSchema(io.swagger.v3.oas.annotations.media.Schema } } + private static boolean shouldUseNodeAsExample(JsonNode node, Schema schemaObject) { + if (node.isObject() || node.isArray()) { + return true; + } + if (schemaObject != null && SchemaTypeUtils.isNumberSchema(schemaObject)) { + return true; + } + if (schemaObject != null && SchemaTypeUtils.isStringSchema(schemaObject)) { + return false; + } + return node.isNumber(); + } + private static void setDefaultSchema(io.swagger.v3.oas.annotations.media.Schema schema, boolean openapi31, Schema schemaObject) { String defaultValue = schema.defaultValue().trim(); final ObjectMapper mapper = openapi31 ? Json31.mapper() : Json.mapper(); @@ -947,6 +961,10 @@ private static void setDefaultSchema(io.swagger.v3.oas.annotations.media.Schema } public static List parseExamplesArray(io.swagger.v3.oas.annotations.media.Schema schema) { + return parseExamplesArray(schema, null); + } + + public static List parseExamplesArray(io.swagger.v3.oas.annotations.media.Schema schema, Schema schemaObject) { String[] examplesArray = schema.examples(); List parsedExamples = new ArrayList<>(); final ObjectMapper mapper = Json31.mapper(); @@ -963,7 +981,9 @@ public static List parseExamplesArray(io.swagger.v3.oas.annotations.medi // Only parse "null" as null value when nullable=true if (node.isNull() && schema.nullable()) { parsedExamples.add(null); - } else if (node.isObject() || node.isArray()) { + } else if (schemaObject == null && "string".equals(schema.type())) { + parsedExamples.add(trimmed); + } else if (shouldUseNodeAsExample(node, schemaObject)) { parsedExamples.add(node); } else { parsedExamples.add(trimmed); diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/Issue5061Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/Issue5061Test.java index 174b88364d..d77781fdd8 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/Issue5061Test.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/converting/Issue5061Test.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.swagger.v3.core.converter.ModelConverters; import io.swagger.v3.core.converter.ResolvedSchema; +import io.swagger.v3.core.util.Json; import io.swagger.v3.core.util.Json31; import org.testng.annotations.Test; @@ -49,6 +50,51 @@ public void testExampleValuesAreSerializedAsJsonDifferentlyBetweenStringAndNumbe assertExampleIsNumber(bigDecimalFieldType); } + @Test + public void testDefaultValueNumericConversionThroughModelResolver() throws Exception { + ResolvedSchema schema = ModelConverters.getInstance(false).readAllAsResolvedSchema( + ModelWithDefaultValues.class + ); + + assertNotNull(schema, "Schema should resolve"); + String json = Json.pretty(schema); + assertNotNull(json); + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(json); + + JsonNode bigDecimalDefault = root.at("/schema/properties/bigDecimalWithDefault"); + assertTrue(bigDecimalDefault.get("default").isNumber(), + "default on BigDecimal field should be serialized as a JSON number"); + + JsonNode stringDefault = root.at("/schema/properties/stringWithDefault"); + assertTrue(stringDefault.get("default").isTextual(), + "default on explicit type=\"string\" field should remain a JSON string"); + } + + @Test + public void testExamplesArrayValuesAreSerializedAsJsonNumbers() throws Exception { + ResolvedSchema schema = ModelConverters.getInstance(true).readAllAsResolvedSchema( + ModelWithExamplesArray.class + ); + + assertNotNull(schema, "Schema should resolve"); + String json = Json31.pretty(schema); + assertNotNull(json); + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(json); + + JsonNode bigDecimalExamples = root.at("/schema/properties/bigDecimalWithExamples"); + assertNotNull(bigDecimalExamples.get("examples"), "examples key should exist in JSON, full node: " + bigDecimalExamples); + assertTrue(bigDecimalExamples.get("examples").get(0).isNumber(), + "examples on BigDecimal field should be serialized as JSON numbers"); + assertTrue(bigDecimalExamples.get("examples").get(1).isNumber(), + "examples on BigDecimal field should be serialized as JSON numbers"); + + JsonNode stringExamples = root.at("/schema/properties/stringWithExamples"); + assertTrue(stringExamples.get("examples").get(0).isTextual(), + "examples on explicit type=\"string\" field should remain JSON strings"); + } + private void assertExampleIsNumber(JsonNode node) { assertTrue(node.get("example").isNumber(), "should be a number"); } @@ -57,6 +103,40 @@ private void assertExampleIsString(JsonNode node) { assertTrue(node.get("example").isTextual(), "should be a string"); } + public static class ModelWithDefaultValues { + + @io.swagger.v3.oas.annotations.media.Schema(defaultValue = "10.00") + BigDecimal bigDecimalWithDefault; + + @io.swagger.v3.oas.annotations.media.Schema(defaultValue = "42", type = "string") + String stringWithDefault; + + public BigDecimal getBigDecimalWithDefault() { + return bigDecimalWithDefault; + } + + public String getStringWithDefault() { + return stringWithDefault; + } + } + + public static class ModelWithExamplesArray { + + @io.swagger.v3.oas.annotations.media.Schema(examples = {"10.00", "20.50"}) + BigDecimal bigDecimalWithExamples; + + @io.swagger.v3.oas.annotations.media.Schema(examples = {"hello", "world"}, type = "string") + String stringWithExamples; + + public BigDecimal getBigDecimalWithExamples() { + return bigDecimalWithExamples; + } + + public String getStringWithExamples() { + return stringWithExamples; + } + } + public static class ModelWithDifferentCombinationOfNumberFieldsWithExamples { @io.swagger.v3.oas.annotations.media.Schema(example = "5 lacs per annum") diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/issues/Issue4339Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/issues/Issue4339Test.java index a45473fd63..beda2391d3 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/issues/Issue4339Test.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/issues/Issue4339Test.java @@ -1,5 +1,6 @@ package io.swagger.v3.core.issues; +import com.fasterxml.jackson.databind.node.IntNode; import com.fasterxml.jackson.databind.node.NullNode; import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.ModelConverterContextImpl; @@ -611,9 +612,9 @@ public void testNullableIntegerWithMultipleExamplesIncludingNull_OAS31() { assertNotNull(integerField.getExamples(), "examples array should exist"); assertEquals(integerField.getExamples().size(), 3, "examples array should have 3 elements"); - assertTrue(integerField.getExamples().contains("1"), "examples should contain '1' as string"); + assertTrue(integerField.getExamples().contains(IntNode.valueOf(1)), "examples should contain 1 as number"); assertTrue(integerField.getExamples().contains(null), "examples should contain null value"); - assertTrue(integerField.getExamples().contains("100"), "examples should contain '100' as string"); + assertTrue(integerField.getExamples().contains(IntNode.valueOf(100)), "examples should contain 100 as number"); } /** diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java index 8c34b51625..15562c387e 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/util/AnnotationsUtilsTest.java @@ -1,5 +1,6 @@ package io.swagger.v3.core.util; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.IntNode; import com.google.common.collect.ImmutableMap; import io.swagger.v3.oas.annotations.ExternalDocumentation; @@ -24,12 +25,17 @@ import java.net.URI; import java.net.URL; import java.util.Date; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.UUID; import static io.swagger.v3.oas.annotations.media.Schema.DEFAULT_SENTINEL; -import static org.testng.Assert.*; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotEquals; +import static org.testng.Assert.assertTrue; +import static org.testng.AssertJUnit.assertNull; public class AnnotationsUtilsTest { @@ -116,6 +122,24 @@ static class ExampleHolder { @io.swagger.v3.oas.annotations.media.Schema(type = "integer", example = "5") String integerValue; + + @io.swagger.v3.oas.annotations.media.Schema(example = "10.00") + BigDecimal bigDecimalValue; + + @io.swagger.v3.oas.annotations.media.Schema(example = "42", type = "string") + String stringWith42; + + @io.swagger.v3.oas.annotations.media.Schema(defaultValue = "10.00") + BigDecimal bigDecimalDefault; + + @io.swagger.v3.oas.annotations.media.Schema(defaultValue = "42", type = "string") + String stringDefaultWith42; + + @io.swagger.v3.oas.annotations.media.Schema(examples = {"10.00", "20.50"}) + BigDecimal bigDecimalExamples; + + @io.swagger.v3.oas.annotations.media.Schema(examples = {"42"}, type = "string") + String stringExamplesWith42; } @Test @@ -661,6 +685,96 @@ public SchemaResolution schemaResolution() { assertNull(schema.get().getDefault()); } + @Test + public void testExampleOnBigDecimalFieldWithoutExplicitTypeShouldBeNumber() throws Exception { + io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = + ExampleHolder.class + .getDeclaredField("bigDecimalValue") + .getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + + Optional schema = + AnnotationsUtils.getSchemaFromAnnotation( + schemaAnnotation, + null, + null, + false, + null, + Schema.SchemaResolution.DEFAULT, + null + ); + + assertTrue(schema.isPresent()); + Object example = schema.get().getExample(); + assertTrue(example instanceof JsonNode, "Example should be a JsonNode, not a plain String"); + assertTrue(((JsonNode) example).isNumber(), "Example should be a numeric node for a BigDecimal field"); + } + + @Test + public void testExampleWithExplicitStringTypeShouldNotProduceNumericNode() throws Exception { + // Regression guard for #4999: explicit type="string" must never yield a numeric node + io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = + ExampleHolder.class + .getDeclaredField("stringWith42") + .getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + + Optional schema = + AnnotationsUtils.getSchemaFromAnnotation( + schemaAnnotation, + null, + null, + false, + null, + Schema.SchemaResolution.DEFAULT, + null + ); + + assertTrue(schema.isPresent()); + Object example = schema.get().getExample(); + assertFalse(example instanceof JsonNode && ((JsonNode) example).isNumber(), + "Example must not be a numeric node when type is explicitly 'string'"); + assertEquals(example, "42", "Example should be the string \"42\""); + } + + @Test + public void testDefaultWithExplicitStringTypeShouldNotProduceNumericNode() throws Exception { + io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = + ExampleHolder.class + .getDeclaredField("stringDefaultWith42") + .getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + + Optional schema = + AnnotationsUtils.getSchemaFromAnnotation( + schemaAnnotation, + null, + null, + false, + null, + Schema.SchemaResolution.DEFAULT, + null + ); + + assertTrue(schema.isPresent()); + Object defaultValue = schema.get().getDefault(); + assertFalse(defaultValue instanceof JsonNode && ((JsonNode) defaultValue).isNumber(), + "Default must not be a numeric node when type is explicitly 'string'"); + assertEquals(defaultValue, "42", "Default should be the string \"42\""); + } + + @Test + public void testExamplesArrayWithExplicitStringTypeShouldNotProduceNumericNodes31() throws Exception { + io.swagger.v3.oas.annotations.media.Schema schemaAnnotation = + ExampleHolder.class + .getDeclaredField("stringExamplesWith42") + .getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class); + + List examples = AnnotationsUtils.parseExamplesArray(schemaAnnotation); + + assertEquals(examples.size(), 1); + assertFalse(examples.get(0) instanceof JsonNode && ((JsonNode) examples.get(0)).isNumber(), + "Example must not be a numeric JsonNode"); + assertEquals(examples.get(0), "42", "Example should be the string \"42\""); + } + // --- mergeSchemaAnnotations defaultValue tests --- @io.swagger.v3.oas.annotations.media.Schema(description = "type-level description")