From abecbe7dbc5fb7e8f3fb404e4f4bb026d2bd30c7 Mon Sep 17 00:00:00 2001 From: Ewa Ostrowska Date: Wed, 26 Aug 2026 15:45:05 +0200 Subject: [PATCH 1/8] fix: correctly resolve schema type in ddTypeWhenSiblingsAllowed (#5235) --- .../v3/core/util/AnnotationsUtils.java | 5 +- .../v3/core/resolving/Ticket5235Test.java | 80 +++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5235Test.java 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 372bbcd5f5..3e8ef50be5 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 @@ -3126,7 +3126,10 @@ public static AnnotatedType addTypeWhenSiblingsAllowed(AnnotatedType aType, io.s if (!Void.class.equals(ctxSchema.implementation())) { aType.setType(ctxSchema.implementation()); } else if (StringUtils.isNotBlank(ctxSchema.type())) { - aType.setType(ctxSchema.type().getClass()); + Type schemaType = getSchemaType(ctxSchema, true); + if (schemaType != null) { + aType.setType(schemaType); + } } } return aType; diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5235Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5235Test.java new file mode 100644 index 0000000000..135d10b5ad --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5235Test.java @@ -0,0 +1,80 @@ +package io.swagger.v3.core.resolving; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverterContextImpl; +import io.swagger.v3.core.jackson.ModelResolver; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.core.util.Json31; +import io.swagger.v3.oas.annotations.media.Schema; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.Collections; + +import static org.testng.Assert.assertEquals; + +public class Ticket5235Test extends SwaggerTestBase { + + @Test + public void testExplicitNonStringTypesInOpenApi31() { + io.swagger.v3.oas.models.media.Schema schema = resolve(true, null); + + assertSerializedTypes(Json31.mapper().valueToTree(schema)); + assertEquals(property(schema, "booleanOverride").getTypes(), Collections.singleton("boolean")); + assertEquals(property(schema, "numberOverride").getTypes(), Collections.singleton("number")); + assertEquals(property(schema, "integerOverride").getTypes(), Collections.singleton("integer")); + } + + @Test(dataProvider = "siblingSchemaResolutions") + public void testExplicitNonStringTypesWithSiblingsEnabled( + io.swagger.v3.oas.models.media.Schema.SchemaResolution schemaResolution) { + io.swagger.v3.oas.models.media.Schema schema = resolve(false, schemaResolution); + + assertSerializedTypes(Json.mapper().valueToTree(schema)); + assertEquals(property(schema, "booleanOverride").getType(), "boolean"); + assertEquals(property(schema, "numberOverride").getType(), "number"); + assertEquals(property(schema, "integerOverride").getType(), "integer"); + } + + @DataProvider + public Object[][] siblingSchemaResolutions() { + return new Object[][] { + {io.swagger.v3.oas.models.media.Schema.SchemaResolution.ALL_OF}, + {io.swagger.v3.oas.models.media.Schema.SchemaResolution.ALL_OF_REF} + }; + } + + private io.swagger.v3.oas.models.media.Schema resolve( + boolean openapi31, io.swagger.v3.oas.models.media.Schema.SchemaResolution schemaResolution) { + ModelResolver resolver = new ModelResolver(mapper()).openapi31(openapi31); + if (schemaResolution != null) { + resolver.schemaResolution(schemaResolution); + } + return new ModelConverterContextImpl(resolver).resolve(new AnnotatedType(ExplicitTypes.class)); + } + + private void assertSerializedTypes(JsonNode schema) { + JsonNode properties = schema.path("properties"); + assertEquals(properties.path("booleanOverride").path("type").asText(), "boolean"); + assertEquals(properties.path("numberOverride").path("type").asText(), "number"); + assertEquals(properties.path("integerOverride").path("type").asText(), "integer"); + } + + private io.swagger.v3.oas.models.media.Schema property( + io.swagger.v3.oas.models.media.Schema schema, String propertyName) { + return (io.swagger.v3.oas.models.media.Schema) schema.getProperties().get(propertyName); + } + + static class ExplicitTypes { + + @Schema(type = "boolean") + public Integer booleanOverride; + + @Schema(type = "number") + public String numberOverride; + + @Schema(type = "integer") + public Boolean integerOverride; + } +} From d236e3f0201ca0856b2a9c87267d590ddea4b022 Mon Sep 17 00:00:00 2001 From: Ewa Ostrowska Date: Thu, 27 Aug 2026 11:09:55 +0200 Subject: [PATCH 2/8] clear derived format for explicit type --- .../swagger/v3/core/jackson/ModelResolver.java | 18 ++++++++++++++++++ .../v3/core/resolving/Ticket5235Test.java | 11 +++++++++++ 2 files changed, 29 insertions(+) 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 b281d34855..f8512cae0c 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 @@ -811,6 +811,7 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context aType = AnnotationsUtils.addTypeWhenSiblingsAllowed(aType, ctxSchema, areSiblingsAllowed); property = context.resolve(aType); property = clone(property); + clearDerivedFormatForExplicitType(property, ctxSchema, areSiblingsAllowed); Schema ctxProperty = null; if (!applySchemaResolution()) { Optional reResolvedProperty = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, property, schemaResolution, context); @@ -1308,6 +1309,23 @@ private Schema clone(Schema property) { return AnnotationsUtils.clone(property, openapi31); } + private static void clearDerivedFormatForExplicitType( + Schema property, + io.swagger.v3.oas.annotations.media.Schema schemaAnnotation, + boolean siblingsAllowed) { + if (property == null || schemaAnnotation == null || !siblingsAllowed) { + return; + } + + boolean hasExplicitImplementation = !Void.class.equals(schemaAnnotation.implementation()); + boolean hasExplicitType = StringUtils.isNotBlank(schemaAnnotation.type()); + boolean hasExplicitFormat = StringUtils.isNotBlank(schemaAnnotation.format()); + + if (!hasExplicitImplementation && hasExplicitType && !hasExplicitFormat) { + property.setFormat(null); + } + } + private boolean isSubtype(AnnotatedClass childClass, Class parentClass) { final BeanDescription parentDesc = _mapper.getSerializationConfig().introspectClassAnnotations(parentClass); List subTypes = _intr().findSubtypes(parentDesc.getClassInfo()); diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5235Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5235Test.java index 135d10b5ad..f150435737 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5235Test.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5235Test.java @@ -13,6 +13,8 @@ import java.util.Collections; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; public class Ticket5235Test extends SwaggerTestBase { @@ -24,6 +26,8 @@ public void testExplicitNonStringTypesInOpenApi31() { assertEquals(property(schema, "booleanOverride").getTypes(), Collections.singleton("boolean")); assertEquals(property(schema, "numberOverride").getTypes(), Collections.singleton("number")); assertEquals(property(schema, "integerOverride").getTypes(), Collections.singleton("integer")); + assertNull(property(schema, "integerOverride").getFormat(), "format must not leak from mapped Java class"); + assertEquals(property(schema, "integerWithFormatOverride").getFormat(), "int32"); } @Test(dataProvider = "siblingSchemaResolutions") @@ -35,6 +39,8 @@ public void testExplicitNonStringTypesWithSiblingsEnabled( assertEquals(property(schema, "booleanOverride").getType(), "boolean"); assertEquals(property(schema, "numberOverride").getType(), "number"); assertEquals(property(schema, "integerOverride").getType(), "integer"); + assertNull(property(schema, "integerOverride").getFormat(), "format must not leak from mapped Java class"); + assertEquals(property(schema, "integerWithFormatOverride").getFormat(), "int32"); } @DataProvider @@ -59,6 +65,8 @@ private void assertSerializedTypes(JsonNode schema) { assertEquals(properties.path("booleanOverride").path("type").asText(), "boolean"); assertEquals(properties.path("numberOverride").path("type").asText(), "number"); assertEquals(properties.path("integerOverride").path("type").asText(), "integer"); + assertTrue(properties.path("integerOverride").path("format").isMissingNode(), "spurious format must be absent"); + assertEquals(properties.path("integerWithFormatOverride").path("format").asText(), "int32"); } private io.swagger.v3.oas.models.media.Schema property( @@ -76,5 +84,8 @@ static class ExplicitTypes { @Schema(type = "integer") public Boolean integerOverride; + + @Schema(type = "integer", format = "int32") + public Boolean integerWithFormatOverride; } } From 87613ce89005294cf04369648f9022f4aa494a83 Mon Sep 17 00:00:00 2001 From: Ewa Ostrowska Date: Thu, 27 Aug 2026 11:32:40 +0200 Subject: [PATCH 3/8] Revert "clear derived format for explicit type" This reverts commit a23dbf2260be01e7194b692b30e8173bf5fa5784. --- .../swagger/v3/core/jackson/ModelResolver.java | 18 ------------------ .../v3/core/resolving/Ticket5235Test.java | 11 ----------- 2 files changed, 29 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 f8512cae0c..b281d34855 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 @@ -811,7 +811,6 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context aType = AnnotationsUtils.addTypeWhenSiblingsAllowed(aType, ctxSchema, areSiblingsAllowed); property = context.resolve(aType); property = clone(property); - clearDerivedFormatForExplicitType(property, ctxSchema, areSiblingsAllowed); Schema ctxProperty = null; if (!applySchemaResolution()) { Optional reResolvedProperty = AnnotationsUtils.getSchemaFromAnnotation(ctxSchema, annotatedType.getComponents(), null, openapi31, property, schemaResolution, context); @@ -1309,23 +1308,6 @@ private Schema clone(Schema property) { return AnnotationsUtils.clone(property, openapi31); } - private static void clearDerivedFormatForExplicitType( - Schema property, - io.swagger.v3.oas.annotations.media.Schema schemaAnnotation, - boolean siblingsAllowed) { - if (property == null || schemaAnnotation == null || !siblingsAllowed) { - return; - } - - boolean hasExplicitImplementation = !Void.class.equals(schemaAnnotation.implementation()); - boolean hasExplicitType = StringUtils.isNotBlank(schemaAnnotation.type()); - boolean hasExplicitFormat = StringUtils.isNotBlank(schemaAnnotation.format()); - - if (!hasExplicitImplementation && hasExplicitType && !hasExplicitFormat) { - property.setFormat(null); - } - } - private boolean isSubtype(AnnotatedClass childClass, Class parentClass) { final BeanDescription parentDesc = _mapper.getSerializationConfig().introspectClassAnnotations(parentClass); List subTypes = _intr().findSubtypes(parentDesc.getClassInfo()); diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5235Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5235Test.java index f150435737..135d10b5ad 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5235Test.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5235Test.java @@ -13,8 +13,6 @@ import java.util.Collections; import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertNull; -import static org.testng.Assert.assertTrue; public class Ticket5235Test extends SwaggerTestBase { @@ -26,8 +24,6 @@ public void testExplicitNonStringTypesInOpenApi31() { assertEquals(property(schema, "booleanOverride").getTypes(), Collections.singleton("boolean")); assertEquals(property(schema, "numberOverride").getTypes(), Collections.singleton("number")); assertEquals(property(schema, "integerOverride").getTypes(), Collections.singleton("integer")); - assertNull(property(schema, "integerOverride").getFormat(), "format must not leak from mapped Java class"); - assertEquals(property(schema, "integerWithFormatOverride").getFormat(), "int32"); } @Test(dataProvider = "siblingSchemaResolutions") @@ -39,8 +35,6 @@ public void testExplicitNonStringTypesWithSiblingsEnabled( assertEquals(property(schema, "booleanOverride").getType(), "boolean"); assertEquals(property(schema, "numberOverride").getType(), "number"); assertEquals(property(schema, "integerOverride").getType(), "integer"); - assertNull(property(schema, "integerOverride").getFormat(), "format must not leak from mapped Java class"); - assertEquals(property(schema, "integerWithFormatOverride").getFormat(), "int32"); } @DataProvider @@ -65,8 +59,6 @@ private void assertSerializedTypes(JsonNode schema) { assertEquals(properties.path("booleanOverride").path("type").asText(), "boolean"); assertEquals(properties.path("numberOverride").path("type").asText(), "number"); assertEquals(properties.path("integerOverride").path("type").asText(), "integer"); - assertTrue(properties.path("integerOverride").path("format").isMissingNode(), "spurious format must be absent"); - assertEquals(properties.path("integerWithFormatOverride").path("format").asText(), "int32"); } private io.swagger.v3.oas.models.media.Schema property( @@ -84,8 +76,5 @@ static class ExplicitTypes { @Schema(type = "integer") public Boolean integerOverride; - - @Schema(type = "integer", format = "int32") - public Boolean integerWithFormatOverride; } } From df3c7f4e198c193dd893e875d064f068e0dad391 Mon Sep 17 00:00:00 2001 From: Ewa Ostrowska Date: Thu, 27 Aug 2026 12:48:35 +0200 Subject: [PATCH 4/8] add tests --- .../v3/core/resolving/Ticket5233Test.java | 238 ++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java new file mode 100644 index 0000000000..da76f183e2 --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java @@ -0,0 +1,238 @@ +package io.swagger.v3.core.resolving; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverters; +import io.swagger.v3.core.converter.ResolvedSchema; +import io.swagger.v3.core.util.Json31; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.models.media.Schema.SchemaResolution; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; + +import static io.swagger.v3.oas.models.media.Schema.BIND_TYPE_AND_TYPES; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +/** + * Regression tests for https://github.com/swagger-api/swagger-core/issues/5233 + * + * Root symptom: @Schema(type="number"|"integer"|"boolean") in OAS 3.1 mode produced + * getType()="number" but getTypes()=["string"] — correct scalar, wrong set. + * The OAS 3.1 serializer reads the types set, so every explicitly typed property + * was emitted as "string". + */ +public class Ticket5233Test extends SwaggerTestBase { + + private String previousBindType; + + @BeforeMethod + public void clearBindType() { + previousBindType = System.getProperty(BIND_TYPE_AND_TYPES); + System.clearProperty(BIND_TYPE_AND_TYPES); + } + + @AfterMethod(alwaysRun = true) + public void restoreBindType() { + if (previousBindType == null) { + System.clearProperty(BIND_TYPE_AND_TYPES); + } else { + System.setProperty(BIND_TYPE_AND_TYPES, previousBindType); + } + } + + // --- OAS 3.1 via ModelConverters (exact repro from issue) --- + + @Test(description = "OAS 3.1: explicit scalar types resolve correctly in types set") + public void testExplicitTypesInOas31ViaModelConverters() { + ResolvedSchema resolved = new ModelConverters(true) + .resolveAsResolvedSchema(new AnnotatedType(Dto.class)); + + io.swagger.v3.oas.models.media.Schema schema = resolved.schema; + + // control: inferred type must be unaffected + assertEquals(typesOf(schema, "inferred"), Collections.singleton("number")); + assertNull(schemaOf(schema, "inferred").getType(), "inferred scalar type must be null in OAS 3.1"); + + // core bug cases + assertEquals(typesOf(schema, "amount"), Collections.singleton("number"), + "amount: getTypes() was [\"string\"] before fix"); + assertEquals(typesOf(schema, "count"), Collections.singleton("integer"), + "count: getTypes() was [\"string\"] before fix"); + assertEquals(typesOf(schema, "flag"), Collections.singleton("boolean"), + "flag: getTypes() was [\"string\"] before fix"); + + // control: enum + assertEquals(typesOf(schema, "unit"), Collections.singleton("string")); + } + + @Test(description = "OAS 3.1: serialized JSON contains correct types") + public void testOas31SerializedOutput() { + ResolvedSchema resolved = new ModelConverters(true) + .resolveAsResolvedSchema(new AnnotatedType(Dto.class)); + + JsonNode root = Json31.mapper().valueToTree(resolved.schema); + JsonNode props = root.path("properties"); + + assertEquals(props.path("inferred").path("type").asText(), "number"); + assertEquals(props.path("amount").path("type").asText(), "number"); + assertEquals(props.path("count").path("type").asText(), "integer"); + assertEquals(props.path("flag").path("type").asText(), "boolean"); + assertEquals(props.path("unit").path("type").asText(), "string"); + + System.out.println(Json31.pretty(resolved.schema)); + } + + @Test(description = "OAS 3.1: nullable scalar types serialize as arrays, including when type is inferred") + public void testNullableTypesInOas31() { + ResolvedSchema resolved = new ModelConverters(true) + .resolveAsResolvedSchema(new AnnotatedType(Dto.class)); + + io.swagger.v3.oas.models.media.Schema schema = resolved.schema; + java.util.Set expectedTypes = new LinkedHashSet<>(Arrays.asList("number", "null")); + + assertEquals(typesOf(schema, "nullableAmount"), expectedTypes); + assertEquals(typesOf(schema, "nullableInferred"), expectedTypes); + assertNull(schemaOf(schema, "nullableAmount").getNullable()); + assertNull(schemaOf(schema, "nullableInferred").getNullable()); + + JsonNode properties = Json31.mapper().valueToTree(schema).path("properties"); + assertSerializedTypes(properties.path("nullableAmount").path("type"), "number", "null"); + assertSerializedTypes(properties.path("nullableInferred").path("type"), "number", "null"); + } + + @Test(description = "bind-type only exposes a singleton types value through the legacy getType accessor") + public void testBindTypeCompatibilityGetter() { + io.swagger.v3.oas.models.media.Schema singleton = new io.swagger.v3.oas.models.media.Schema() + .types(Collections.singleton("string")); + io.swagger.v3.oas.models.media.Schema multiple = new io.swagger.v3.oas.models.media.Schema() + .types(new LinkedHashSet<>(Arrays.asList("string", "null"))); + io.swagger.v3.oas.models.media.Schema conflicting = new io.swagger.v3.oas.models.media.Schema() + .type("integer") + .types(Collections.singleton("string")); + io.swagger.v3.oas.models.media.Schema empty = new io.swagger.v3.oas.models.media.Schema(); + + assertNull(singleton.getType()); + + System.setProperty(BIND_TYPE_AND_TYPES, "true"); + + assertEquals(singleton.getType(), "string"); + assertEquals(singleton.getTypes(), Collections.singleton("string")); + assertNull(multiple.getType(), "multiple OAS 3.1 types cannot be represented by the legacy getter"); + assertEquals(conflicting.getType(), "integer", "the explicit legacy type must take precedence"); + assertEquals(conflicting.getTypes(), Collections.singleton("string"), "bind-type must not synchronize fields"); + assertNull(empty.getType()); + + System.setProperty(BIND_TYPE_AND_TYPES, "false"); + assertNull(singleton.getType(), "getType must read bind-type on each invocation"); + } + + @Test(description = "bind-type does not change OAS 3.1 types or serialization") + public void testBindTypeDoesNotAffectOas31Serialization() { + ResolvedSchema resolved = new ModelConverters(true) + .resolveAsResolvedSchema(new AnnotatedType(Dto.class)); + io.swagger.v3.oas.models.media.Schema schema = resolved.schema; + io.swagger.v3.oas.models.media.Schema inferred = schemaOf(schema, "inferred"); + io.swagger.v3.oas.models.media.Schema nullable = schemaOf(schema, "nullableInferred"); + JsonNode withoutBinding = Json31.mapper().valueToTree(schema); + + assertNull(inferred.getType()); + assertNull(nullable.getType()); + + System.setProperty(BIND_TYPE_AND_TYPES, "true"); + + assertEquals(inferred.getType(), "number"); + assertNull(nullable.getType(), "nullable schema has both number and null types"); + assertEquals(typesOf(schema, "inferred"), Collections.singleton("number")); + assertEquals(typesOf(schema, "nullableInferred"), + new LinkedHashSet<>(Arrays.asList("number", "null"))); + assertEquals(Json31.mapper().valueToTree(schema), withoutBinding, + "bind-type must not affect OAS 3.1 serialization"); + } + + @Test(description = "OAS 3.0 default mode: still correct (was working before regression)") + public void testExplicitTypesInOas30DefaultMode() { + ResolvedSchema resolved = new ModelConverters(false) + .resolveAsResolvedSchema(new AnnotatedType(Dto.class)); + + io.swagger.v3.oas.models.media.Schema schema = resolved.schema; + + assertEquals(schemaOf(schema, "amount").getType(), "number"); + assertEquals(schemaOf(schema, "count").getType(), "integer"); + assertEquals(schemaOf(schema, "flag").getType(), "boolean"); + assertEquals(schemaOf(schema, "inferred").getType(), "number"); + } + + @Test(description = "OAS 3.0 ALL_OF sibling mode: explicit non-string types resolve correctly") + public void testExplicitTypesInOas30AllOfMode() { + ResolvedSchema resolved = new ModelConverters(false, SchemaResolution.ALL_OF) + .resolveAsResolvedSchema(new AnnotatedType(Dto.class)); + + io.swagger.v3.oas.models.media.Schema schema = resolved.schema; + + assertEquals(schemaOf(schema, "amount").getType(), "number"); + assertEquals(schemaOf(schema, "count").getType(), "integer"); + assertEquals(schemaOf(schema, "flag").getType(), "boolean"); + } + + @Test(description = "OAS 3.0 ALL_OF_REF sibling mode: explicit non-string types resolve correctly") + public void testExplicitTypesInOas30AllOfRefMode() { + ResolvedSchema resolved = new ModelConverters(false, SchemaResolution.ALL_OF_REF) + .resolveAsResolvedSchema(new AnnotatedType(Dto.class)); + + io.swagger.v3.oas.models.media.Schema schema = resolved.schema; + + assertEquals(schemaOf(schema, "amount").getType(), "number"); + assertEquals(schemaOf(schema, "count").getType(), "integer"); + assertEquals(schemaOf(schema, "flag").getType(), "boolean"); + } + + // --- DTO from the issue (field types match the declared @Schema type) --- + + static class Dto { + @Schema(title = "Inferred") + public BigDecimal inferred; + + @Schema(title = "Amount", type = "number") + public BigDecimal amount; + + @Schema(title = "Nullable amount", type = "number", nullable = true) + public BigDecimal nullableAmount; + + @Schema(title = "Nullable inferred", nullable = true) + public BigDecimal nullableInferred; + + @Schema(title = "Count", type = "integer") + public Integer count; + + @Schema(title = "Flag", type = "boolean") + public Boolean flag; + + @Schema(title = "Unit") + public Freq unit; + + enum Freq { DAY, WEEK, MONTH } + } + + @SuppressWarnings("unchecked") + private java.util.Set typesOf(io.swagger.v3.oas.models.media.Schema schema, String property) { + return schemaOf(schema, property).getTypes(); + } + + private io.swagger.v3.oas.models.media.Schema schemaOf( + io.swagger.v3.oas.models.media.Schema schema, String property) { + return (io.swagger.v3.oas.models.media.Schema) schema.getProperties().get(property); + } + + private void assertSerializedTypes(JsonNode types, String first, String second) { + assertEquals(types.size(), 2); + assertEquals(types.path(0).asText(), first); + assertEquals(types.path(1).asText(), second); + } +} From e7b500b8741feb56563c2d69d388c3c81e4be737 Mon Sep 17 00:00:00 2001 From: Ewa Ostrowska Date: Fri, 28 Aug 2026 09:12:49 +0200 Subject: [PATCH 5/8] add tests to document Schema(type) behavior --- .../v3/core/resolving/Ticket5233Test.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java index da76f183e2..b194d1e539 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java @@ -107,6 +107,53 @@ public void testNullableTypesInOas31() { assertSerializedTypes(properties.path("nullableInferred").path("type"), "number", "null"); } + @Test(description = "OAS 3.1: supported type sources and precedence control serialized output") + public void testSupportedTypeSourcesAndPrecedenceInOas31() { + ResolvedSchema resolved = new ModelConverters(true) + .resolveAsResolvedSchema(new AnnotatedType(Dto.class)); + io.swagger.v3.oas.models.media.Schema schema = resolved.schema; + + assertEquals(typesOf(schema, "stringOverride"), Collections.singleton("string")); + assertEquals(typesOf(schema, "int32Override"), Collections.singleton("integer")); + assertEquals(schemaOf(schema, "int32Override").getFormat(), "int32"); + assertEquals(typesOf(schema, "floatOverride"), Collections.singleton("number")); + assertEquals(schemaOf(schema, "floatOverride").getFormat(), "float"); + assertEquals(typesOf(schema, "nullableBooleanOverride"), + new LinkedHashSet<>(Arrays.asList("boolean", "null"))); + + assertEquals(typesOf(schema, "typesOverride"), Collections.singleton("boolean")); + assertEquals(typesOf(schema, "nullableTypesOverride"), + new LinkedHashSet<>(Arrays.asList("integer", "null"))); + + io.swagger.v3.oas.models.media.Schema conflictingTypeAndTypes = + schemaOf(schema, "conflictingTypeAndTypes"); + assertEquals(conflictingTypeAndTypes.getType(), "number"); + assertEquals(conflictingTypeAndTypes.getTypes(), Collections.singleton("boolean"), + "OAS 3.1 serialization uses types(), independently of the legacy type()"); + + assertEquals(typesOf(schema, "implementationOverride"), Collections.singleton("integer")); + assertEquals(schemaOf(schema, "implementationOverride").getFormat(), "int64"); + + io.swagger.v3.oas.models.media.Schema conflictingImplementationAndType = + schemaOf(schema, "conflictingImplementationAndType"); + assertEquals(conflictingImplementationAndType.getType(), "boolean"); + assertEquals(conflictingImplementationAndType.getTypes(), Collections.singleton("integer"), + "implementation() controls resolution when it conflicts with type()"); + + JsonNode properties = Json31.mapper().valueToTree(schema).path("properties"); + assertEquals(properties.path("stringOverride").path("type").asText(), "string"); + assertEquals(properties.path("int32Override").path("type").asText(), "integer"); + assertEquals(properties.path("int32Override").path("format").asText(), "int32"); + assertEquals(properties.path("floatOverride").path("type").asText(), "number"); + assertEquals(properties.path("floatOverride").path("format").asText(), "float"); + assertSerializedTypes(properties.path("nullableBooleanOverride").path("type"), "boolean", "null"); + assertEquals(properties.path("typesOverride").path("type").asText(), "boolean"); + assertSerializedTypes(properties.path("nullableTypesOverride").path("type"), "integer", "null"); + assertEquals(properties.path("conflictingTypeAndTypes").path("type").asText(), "boolean"); + assertEquals(properties.path("implementationOverride").path("type").asText(), "integer"); + assertEquals(properties.path("conflictingImplementationAndType").path("type").asText(), "integer"); + } + @Test(description = "bind-type only exposes a singleton types value through the legacy getType accessor") public void testBindTypeCompatibilityGetter() { io.swagger.v3.oas.models.media.Schema singleton = new io.swagger.v3.oas.models.media.Schema() @@ -214,6 +261,33 @@ static class Dto { @Schema(title = "Flag", type = "boolean") public Boolean flag; + @Schema(title = "String override", type = "string") + public Integer stringOverride; + + @Schema(title = "Int32 override", type = "integer", format = "int32") + public String int32Override; + + @Schema(title = "Float override", type = "number", format = "float") + public String floatOverride; + + @Schema(title = "Nullable boolean override", type = "boolean", nullable = true) + public String nullableBooleanOverride; + + @Schema(title = "Types override", types = {"boolean"}) + public String typesOverride; + + @Schema(title = "Nullable types override", types = {"integer"}, nullable = true) + public String nullableTypesOverride; + + @Schema(title = "Conflicting type and types", type = "number", types = {"boolean"}) + public String conflictingTypeAndTypes; + + @Schema(title = "Implementation override", implementation = Long.class) + public String implementationOverride; + + @Schema(title = "Conflicting implementation and type", implementation = Long.class, type = "boolean") + public String conflictingImplementationAndType; + @Schema(title = "Unit") public Freq unit; From cca7f7fa35bbad77b3915e9395f2d3981301ab08 Mon Sep 17 00:00:00 2001 From: forhadmethun Date: Wed, 15 Jul 2026 13:30:49 +0300 Subject: [PATCH 6/8] fix: apply explicit @Schema(type) to the types set under OpenAPI 3.1 Under OpenAPI 3.1 an explicit @Schema(type = ...) was applied via the legacy scalar setType(), while the 3.1 serializer reads the types set. The set kept its default ("string"), so number/integer/boolean fields were rendered as string. Populate the types set when openapi31 is enabled, mirroring the handling of the plural types() attribute directly below. Fixes #5233 --- .../v3/core/util/AnnotationsUtils.java | 6 +- .../v3/core/resolving/v31/Ticket5233Test.java | 60 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/Ticket5233Test.java 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 3e8ef50be5..19e8c65cd5 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 @@ -696,7 +696,11 @@ public static Optional getSchemaFromAnnotation( schemaObject.set$ref(schema.ref()); } if (StringUtils.isNotBlank(schema.type())) { - schemaObject.setType(schema.type()); + if (openapi31) { + schemaObject.setTypes(new LinkedHashSet<>(Arrays.asList(schema.type()))); + } else { + schemaObject.setType(schema.type()); + } } if (schema.types().length > 0) { diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/Ticket5233Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/Ticket5233Test.java new file mode 100644 index 0000000000..6109d6045b --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/Ticket5233Test.java @@ -0,0 +1,60 @@ +package io.swagger.v3.core.resolving.v31; + +import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverterContextImpl; +import io.swagger.v3.core.jackson.ModelResolver; +import io.swagger.v3.core.resolving.SwaggerTestBase; +import io.swagger.v3.oas.models.media.Schema; +import org.testng.annotations.Test; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import static org.testng.Assert.assertEquals; + +/** + * An explicit {@code @Schema(type = "number" | "integer" | "boolean")} must be reflected in the + * OpenAPI 3.1 "types" set, not only in the legacy scalar "type" field. Previously the explicit type + * was applied via {@code setType} (scalar) while the "types" set kept the default {@code ["string"]}, + * so the serialized 3.1 schema rendered every explicitly-typed field as {@code string}. + */ +public class Ticket5233Test extends SwaggerTestBase { + + @Test + public void testExplicitSchemaTypeIsAppliedToTypesSetUnderOpenApi31() { + final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + final Schema model = context.resolve(new AnnotatedType(Model.class)); + + final Map properties = model.getProperties(); + assertEquals(properties.get("amount").getTypes(), Collections.singleton("number")); + assertEquals(properties.get("count").getTypes(), Collections.singleton("integer")); + assertEquals(properties.get("flag").getTypes(), Collections.singleton("boolean")); + + // controls: inferred type and enum are unaffected + assertEquals(properties.get("inferred").getTypes(), Collections.singleton("number")); + assertEquals(properties.get("unit").getTypes(), Collections.singleton("string")); + assertEquals(properties.get("unit").getEnum(), Arrays.asList("DAY", "WEEK", "MONTH")); + } + + enum Freq {DAY, WEEK, MONTH} + + private static class Model { + @io.swagger.v3.oas.annotations.media.Schema(title = "Inferred") + public BigDecimal inferred; + + @io.swagger.v3.oas.annotations.media.Schema(title = "Amount", type = "number") + public BigDecimal amount; + + @io.swagger.v3.oas.annotations.media.Schema(title = "Count", type = "integer") + public Integer count; + + @io.swagger.v3.oas.annotations.media.Schema(title = "Flag", type = "boolean") + public Boolean flag; + + @io.swagger.v3.oas.annotations.media.Schema(title = "Unit") + public Freq unit; + } +} From e2f9c2d9f6da0a84a5b8806b3d980cbc52e9b055 Mon Sep 17 00:00:00 2001 From: Ewa Ostrowska Date: Fri, 28 Aug 2026 12:13:48 +0200 Subject: [PATCH 7/8] apply explicit @Schema(type) to the types set under OpenAPI 3.1 --- .../v3/core/util/AnnotationsUtils.java | 7 +- .../v3/core/resolving/Ticket5233Test.java | 113 +++++++++++++++++- 2 files changed, 114 insertions(+), 6 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 19e8c65cd5..671adf27c5 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 @@ -50,6 +50,7 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -696,9 +697,9 @@ public static Optional getSchemaFromAnnotation( schemaObject.set$ref(schema.ref()); } if (StringUtils.isNotBlank(schema.type())) { - if (openapi31) { - schemaObject.setTypes(new LinkedHashSet<>(Arrays.asList(schema.type()))); - } else { + if (openapi31 && Void.class.equals(schema.implementation())) { + schemaObject.setTypes(new LinkedHashSet<>(Collections.singleton(schema.type()))); + } else if (!openapi31) { schemaObject.setType(schema.type()); } } diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java index b194d1e539..92a2c2c6ff 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java @@ -4,6 +4,7 @@ import io.swagger.v3.core.converter.AnnotatedType; import io.swagger.v3.core.converter.ModelConverters; import io.swagger.v3.core.converter.ResolvedSchema; +import io.swagger.v3.core.util.AnnotationsUtils; import io.swagger.v3.core.util.Json31; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.models.media.Schema.SchemaResolution; @@ -120,6 +121,9 @@ public void testSupportedTypeSourcesAndPrecedenceInOas31() { assertEquals(schemaOf(schema, "floatOverride").getFormat(), "float"); assertEquals(typesOf(schema, "nullableBooleanOverride"), new LinkedHashSet<>(Arrays.asList("boolean", "null"))); + assertEquals(typesOf(schema, "objectOverride"), Collections.singleton("object")); + assertEquals(typesOf(schema, "arrayOverride"), Collections.singleton("array")); + assertEquals(typesOf(schema, "nullOverride"), Collections.singleton("null")); assertEquals(typesOf(schema, "typesOverride"), Collections.singleton("boolean")); assertEquals(typesOf(schema, "nullableTypesOverride"), @@ -127,18 +131,20 @@ public void testSupportedTypeSourcesAndPrecedenceInOas31() { io.swagger.v3.oas.models.media.Schema conflictingTypeAndTypes = schemaOf(schema, "conflictingTypeAndTypes"); - assertEquals(conflictingTypeAndTypes.getType(), "number"); + assertNull(conflictingTypeAndTypes.getType()); assertEquals(conflictingTypeAndTypes.getTypes(), Collections.singleton("boolean"), - "OAS 3.1 serialization uses types(), independently of the legacy type()"); + "OAS 3.1 serialization uses types(), without populating the legacy type()"); assertEquals(typesOf(schema, "implementationOverride"), Collections.singleton("integer")); assertEquals(schemaOf(schema, "implementationOverride").getFormat(), "int64"); io.swagger.v3.oas.models.media.Schema conflictingImplementationAndType = schemaOf(schema, "conflictingImplementationAndType"); - assertEquals(conflictingImplementationAndType.getType(), "boolean"); + assertNull(conflictingImplementationAndType.getType()); assertEquals(conflictingImplementationAndType.getTypes(), Collections.singleton("integer"), "implementation() controls resolution when it conflicts with type()"); + assertEquals(typesOf(schema, "nullableConflictingImplementationAndType"), + new LinkedHashSet<>(Arrays.asList("integer", "null"))); JsonNode properties = Json31.mapper().valueToTree(schema).path("properties"); assertEquals(properties.path("stringOverride").path("type").asText(), "string"); @@ -147,11 +153,61 @@ public void testSupportedTypeSourcesAndPrecedenceInOas31() { assertEquals(properties.path("floatOverride").path("type").asText(), "number"); assertEquals(properties.path("floatOverride").path("format").asText(), "float"); assertSerializedTypes(properties.path("nullableBooleanOverride").path("type"), "boolean", "null"); + assertEquals(properties.path("objectOverride").path("type").asText(), "object"); + assertEquals(properties.path("arrayOverride").path("type").asText(), "array"); + assertEquals(properties.path("nullOverride").path("type").asText(), "null"); assertEquals(properties.path("typesOverride").path("type").asText(), "boolean"); assertSerializedTypes(properties.path("nullableTypesOverride").path("type"), "integer", "null"); assertEquals(properties.path("conflictingTypeAndTypes").path("type").asText(), "boolean"); assertEquals(properties.path("implementationOverride").path("type").asText(), "integer"); assertEquals(properties.path("conflictingImplementationAndType").path("type").asText(), "integer"); + assertSerializedTypes(properties.path("nullableConflictingImplementationAndType").path("type"), "integer", "null"); + } + + @Test(description = "OAS 3.1: type is copied to types without an inferred property schema") + public void testTypeIsCopiedToTypesForAnnotationOnlyResolution() throws NoSuchFieldException { + assertAnnotationOnlyType("objectType", "object"); + assertAnnotationOnlyType("arrayType", "array"); + assertAnnotationOnlyType("nullType", "null"); + } + + @Test(description = "OAS 3.1: a blank annotation type does not populate types") + public void testBlankTypeForAnnotationOnlyResolution() throws NoSuchFieldException { + io.swagger.v3.oas.models.media.Schema schema = annotationOnlySchema("blankType", true); + + assertNull(schema.getType()); + assertNull(schema.getTypes()); + assertEquals(Json31.mapper().valueToTree(schema).has("type"), false); + } + + @Test(description = "OAS 3.1: nullable is added to blank and explicit annotation types") + public void testNullableTypesForAnnotationOnlyResolution() throws NoSuchFieldException { + io.swagger.v3.oas.models.media.Schema nullableBlank = annotationOnlySchema("nullableBlankType", true); + assertNull(nullableBlank.getType()); + assertEquals(nullableBlank.getTypes(), Collections.singleton("null")); + assertEquals(Json31.mapper().valueToTree(nullableBlank).path("type").asText(), "null"); + + io.swagger.v3.oas.models.media.Schema nullableNumber = annotationOnlySchema("nullableNumberType", true); + assertNull(nullableNumber.getType()); + assertEquals(nullableNumber.getTypes(), new LinkedHashSet<>(Arrays.asList("number", "null"))); + assertSerializedTypes(Json31.mapper().valueToTree(nullableNumber).path("type"), "number", "null"); + } + + @Test(description = "OAS 3.1: explicit types take precedence over type before nullable is added") + public void testExplicitTypesPrecedenceForAnnotationOnlyResolution() throws NoSuchFieldException { + io.swagger.v3.oas.models.media.Schema schema = annotationOnlySchema("nullableTypesOverride", true); + + assertNull(schema.getType()); + assertEquals(schema.getTypes(), new LinkedHashSet<>(Arrays.asList("boolean", "null"))); + assertSerializedTypes(Json31.mapper().valueToTree(schema).path("type"), "boolean", "null"); + } + + @Test(description = "OAS 3.0: type is not copied to the OAS 3.1 types set") + public void testTypeIsNotCopiedToTypesForOas30() throws NoSuchFieldException { + io.swagger.v3.oas.models.media.Schema schema = annotationOnlySchema("objectType", false); + + assertEquals(schema.getType(), "object"); + assertNull(schema.getTypes()); } @Test(description = "bind-type only exposes a singleton types value through the legacy getType accessor") @@ -273,6 +329,15 @@ static class Dto { @Schema(title = "Nullable boolean override", type = "boolean", nullable = true) public String nullableBooleanOverride; + @Schema(title = "Object override", type = "object") + public String objectOverride; + + @Schema(title = "Array override", type = "array") + public String arrayOverride; + + @Schema(title = "Null override", type = "null") + public String nullOverride; + @Schema(title = "Types override", types = {"boolean"}) public String typesOverride; @@ -288,12 +353,39 @@ static class Dto { @Schema(title = "Conflicting implementation and type", implementation = Long.class, type = "boolean") public String conflictingImplementationAndType; + @Schema(title = "Nullable conflicting implementation and type", implementation = Long.class, + type = "boolean", nullable = true) + public String nullableConflictingImplementationAndType; + @Schema(title = "Unit") public Freq unit; enum Freq { DAY, WEEK, MONTH } } + static class AnnotationOnlyTypes { + @Schema(type = "object") + public Object objectType; + + @Schema(type = "array") + public Object arrayType; + + @Schema(type = "null") + public Object nullType; + + @Schema(title = "Blank type") + public Object blankType; + + @Schema(title = "Nullable blank type", nullable = true) + public Object nullableBlankType; + + @Schema(type = "number", nullable = true) + public Object nullableNumberType; + + @Schema(type = "number", types = {"boolean"}, nullable = true) + public Object nullableTypesOverride; + } + @SuppressWarnings("unchecked") private java.util.Set typesOf(io.swagger.v3.oas.models.media.Schema schema, String property) { return schemaOf(schema, property).getTypes(); @@ -309,4 +401,19 @@ private void assertSerializedTypes(JsonNode types, String first, String second) assertEquals(types.path(0).asText(), first); assertEquals(types.path(1).asText(), second); } + + private void assertAnnotationOnlyType(String fieldName, String expectedType) throws NoSuchFieldException { + io.swagger.v3.oas.models.media.Schema schema = annotationOnlySchema(fieldName, true); + + assertNull(schema.getType()); + assertEquals(schema.getTypes(), Collections.singleton(expectedType)); + assertEquals(Json31.mapper().valueToTree(schema).path("type").asText(), expectedType); + } + + private io.swagger.v3.oas.models.media.Schema annotationOnlySchema(String fieldName, boolean openapi31) + throws NoSuchFieldException { + Schema annotation = AnnotationOnlyTypes.class.getField(fieldName).getAnnotation(Schema.class); + return AnnotationsUtils.getSchemaFromAnnotation(annotation, null, null, openapi31) + .orElseThrow(AssertionError::new); + } } From e0e5fb9429d537322ef9588f152332317d2ce302 Mon Sep 17 00:00:00 2001 From: Ewa Ostrowska Date: Fri, 28 Aug 2026 12:17:16 +0200 Subject: [PATCH 8/8] apply explicit @Schema(type) to the types set under OpenAPI 3.1 --- .../v3/core/resolving/Ticket5233Test.java | 20 +++++++ .../v3/core/resolving/v31/Ticket5233Test.java | 60 ------------------- 2 files changed, 20 insertions(+), 60 deletions(-) delete mode 100644 modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/Ticket5233Test.java diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java index 92a2c2c6ff..ebe24028fa 100644 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5233Test.java @@ -2,8 +2,10 @@ import com.fasterxml.jackson.databind.JsonNode; import io.swagger.v3.core.converter.AnnotatedType; +import io.swagger.v3.core.converter.ModelConverterContextImpl; import io.swagger.v3.core.converter.ModelConverters; import io.swagger.v3.core.converter.ResolvedSchema; +import io.swagger.v3.core.jackson.ModelResolver; import io.swagger.v3.core.util.AnnotationsUtils; import io.swagger.v3.core.util.Json31; import io.swagger.v3.oas.annotations.media.Schema; @@ -16,6 +18,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; +import java.util.Map; import static io.swagger.v3.oas.models.media.Schema.BIND_TYPE_AND_TYPES; import static org.testng.Assert.assertEquals; @@ -73,6 +76,23 @@ public void testExplicitTypesInOas31ViaModelConverters() { assertEquals(typesOf(schema, "unit"), Collections.singleton("string")); } + @Test(description = "OAS 3.1: explicit scalar types resolve correctly via ModelResolver") + public void testExplicitTypesInOas31ViaModelResolver() { + ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true); + ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(Dto.class)); + + Map properties = schema.getProperties(); + assertEquals(properties.get("amount").getTypes(), Collections.singleton("number")); + assertEquals(properties.get("count").getTypes(), Collections.singleton("integer")); + assertEquals(properties.get("flag").getTypes(), Collections.singleton("boolean")); + + // controls: inferred type and enum are unaffected + assertEquals(properties.get("inferred").getTypes(), Collections.singleton("number")); + assertEquals(properties.get("unit").getTypes(), Collections.singleton("string")); + assertEquals(properties.get("unit").getEnum(), Arrays.asList("DAY", "WEEK", "MONTH")); + } + @Test(description = "OAS 3.1: serialized JSON contains correct types") public void testOas31SerializedOutput() { ResolvedSchema resolved = new ModelConverters(true) diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/Ticket5233Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/Ticket5233Test.java deleted file mode 100644 index 6109d6045b..0000000000 --- a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/v31/Ticket5233Test.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.swagger.v3.core.resolving.v31; - -import io.swagger.v3.core.converter.AnnotatedType; -import io.swagger.v3.core.converter.ModelConverterContextImpl; -import io.swagger.v3.core.jackson.ModelResolver; -import io.swagger.v3.core.resolving.SwaggerTestBase; -import io.swagger.v3.oas.models.media.Schema; -import org.testng.annotations.Test; - -import java.math.BigDecimal; -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; - -import static org.testng.Assert.assertEquals; - -/** - * An explicit {@code @Schema(type = "number" | "integer" | "boolean")} must be reflected in the - * OpenAPI 3.1 "types" set, not only in the legacy scalar "type" field. Previously the explicit type - * was applied via {@code setType} (scalar) while the "types" set kept the default {@code ["string"]}, - * so the serialized 3.1 schema rendered every explicitly-typed field as {@code string}. - */ -public class Ticket5233Test extends SwaggerTestBase { - - @Test - public void testExplicitSchemaTypeIsAppliedToTypesSetUnderOpenApi31() { - final ModelResolver modelResolver = new ModelResolver(mapper()).openapi31(true); - final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); - final Schema model = context.resolve(new AnnotatedType(Model.class)); - - final Map properties = model.getProperties(); - assertEquals(properties.get("amount").getTypes(), Collections.singleton("number")); - assertEquals(properties.get("count").getTypes(), Collections.singleton("integer")); - assertEquals(properties.get("flag").getTypes(), Collections.singleton("boolean")); - - // controls: inferred type and enum are unaffected - assertEquals(properties.get("inferred").getTypes(), Collections.singleton("number")); - assertEquals(properties.get("unit").getTypes(), Collections.singleton("string")); - assertEquals(properties.get("unit").getEnum(), Arrays.asList("DAY", "WEEK", "MONTH")); - } - - enum Freq {DAY, WEEK, MONTH} - - private static class Model { - @io.swagger.v3.oas.annotations.media.Schema(title = "Inferred") - public BigDecimal inferred; - - @io.swagger.v3.oas.annotations.media.Schema(title = "Amount", type = "number") - public BigDecimal amount; - - @io.swagger.v3.oas.annotations.media.Schema(title = "Count", type = "integer") - public Integer count; - - @io.swagger.v3.oas.annotations.media.Schema(title = "Flag", type = "boolean") - public Boolean flag; - - @io.swagger.v3.oas.annotations.media.Schema(title = "Unit") - public Freq unit; - } -}