From f67b8a02ac2bbbc539f03f90fec3a19dbe129fd4 Mon Sep 17 00:00:00 2001 From: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> Date: Sat, 30 May 2026 15:10:10 +0900 Subject: [PATCH] fix: let explicit @Schema(format) override type-derived format (#5185) `@Schema(format = "uri-reference")` on a `java.net.URI` field was ignored because `resolveSchemaMembers` only applied the annotation format when the schema had no format yet. For `java.net.URI` the `PrimitiveType` already sets `format: uri`, so the user-specified `uri-reference` was dropped. An explicit `format` on `@Schema` expresses the user's intent and must take precedence over a format derived from the property type. A URI field without an explicit format still resolves to the default `uri`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../v3/core/jackson/ModelResolver.java | 4 +- .../v3/core/resolving/Ticket5185Test.java | 136 ++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5185Test.java 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 f25f99a654..15683e5e8c 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 @@ -3181,7 +3181,9 @@ protected void resolveSchemaMembers(Schema schema, Annotated a, Annotation[] ann schema.title(title); } String format = resolveFormat(a, annotations, schemaAnnotation); - if (StringUtils.isNotBlank(format) && StringUtils.isBlank(schema.getFormat())) { + if (StringUtils.isNotBlank(format)) { + // An explicit format on @Schema is the user's intent and must take precedence + // over a format derived from the property type (e.g. java.net.URI -> "uri"). schema.format(format); } Object defaultValue = resolveDefaultValue(a, annotations, schemaAnnotation); diff --git a/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5185Test.java b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5185Test.java new file mode 100644 index 0000000000..5a014a637f --- /dev/null +++ b/modules/swagger-core/src/test/java/io/swagger/v3/core/resolving/Ticket5185Test.java @@ -0,0 +1,136 @@ +package io.swagger.v3.core.resolving; + +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.oas.annotations.media.Schema; +import org.testng.annotations.Test; + +import javax.validation.constraints.Email; +import java.net.URI; + +import static io.swagger.v3.core.resolving.SwaggerTestBase.mapper; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +public class Ticket5185Test { + + @Test + public void testExplicitFormatOverridesTypeDerivedFormat() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(UriExample.class)); + + assertNotNull(schema); + io.swagger.v3.oas.models.media.Schema uriReference = + (io.swagger.v3.oas.models.media.Schema) schema.getProperties().get("uriReferenceValue"); + assertNotNull(uriReference); + assertEquals(uriReference.getFormat(), "uri-reference"); + + // A URI field without an explicit @Schema(format=...) still resolves to the default "uri" format + io.swagger.v3.oas.models.media.Schema plainUri = + (io.swagger.v3.oas.models.media.Schema) schema.getProperties().get("uriValue"); + assertNotNull(plainUri); + assertEquals(plainUri.getFormat(), "uri"); + } + + @Test + public void testExplicitFormatPrecedenceForStringAndNumericTypes() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(ExplicitFormatExample.class)); + + assertNotNull(schema); + assertProperty(schema, "uuidValue", "string", "uuid"); + assertProperty(schema, "emailValue", "string", "email"); + assertProperty(schema, "emailValidatedValue", "string", "email"); + assertProperty(schema, "beanValidationEmailValue", "string", "email"); + assertProperty(schema, "longValue", "integer", "int64"); + assertProperty(schema, "integerAsInt64Value", "integer", "int64"); + } + + @Test + public void testExplicitFormatOverridesTypeDerivedFormatOpenApi31() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + modelResolver.setOpenapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(UriExample.class)); + + assertNotNull(schema); + io.swagger.v3.oas.models.media.Schema uriReference = + (io.swagger.v3.oas.models.media.Schema) schema.getProperties().get("uriReferenceValue"); + assertNotNull(uriReference); + assertEquals(uriReference.getFormat(), "uri-reference"); + + io.swagger.v3.oas.models.media.Schema plainUri = + (io.swagger.v3.oas.models.media.Schema) schema.getProperties().get("uriValue"); + assertNotNull(plainUri); + assertEquals(plainUri.getFormat(), "uri"); + } + + @Test + public void testExplicitFormatPrecedenceForStringAndNumericTypesOpenApi31() { + final ModelResolver modelResolver = new ModelResolver(mapper()); + modelResolver.setOpenapi31(true); + final ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + io.swagger.v3.oas.models.media.Schema schema = context.resolve(new AnnotatedType(ExplicitFormatExample.class)); + + assertNotNull(schema); + assertProperty31(schema, "uuidValue", "string", "uuid"); + assertProperty31(schema, "emailValue", "string", "email"); + assertProperty31(schema, "emailValidatedValue", "string", "email"); + assertProperty31(schema, "beanValidationEmailValue", "string", "email"); + assertProperty31(schema, "longValue", "integer", "int64"); + assertProperty31(schema, "integerAsInt64Value", "integer", "int64"); + } + + private static void assertProperty(io.swagger.v3.oas.models.media.Schema schema, String propertyName, String type, String format) { + io.swagger.v3.oas.models.media.Schema property = + (io.swagger.v3.oas.models.media.Schema) schema.getProperties().get(propertyName); + assertNotNull(property); + assertEquals(property.getType(), type); + assertEquals(property.getFormat(), format); + } + + private static void assertProperty31(io.swagger.v3.oas.models.media.Schema schema, String propertyName, String type, String format) { + io.swagger.v3.oas.models.media.Schema property = + (io.swagger.v3.oas.models.media.Schema) schema.getProperties().get(propertyName); + assertNotNull(property); + assertNotNull(property.getTypes()); + assertTrue(property.getTypes().contains(type)); + assertEquals(property.getFormat(), format); + } + + static class UriExample { + @Schema(format = "uri-reference") + public URI uriReferenceValue; + + public URI uriValue; + } + + static class ExplicitFormatExample { + @Schema(format = "uuid") + public String uuidValue; + + @Schema(format = "email") + public String emailValue; + + @Schema(format = "email") + @Email + public String emailValidatedValue; + + @Email + public String beanValidationEmailValue; + + @Schema(format = "int64") + public Long longValue; + + @Schema(format = "int64") + public Integer integerAsInt64Value; + } +}