From b87d8fc669f7d434a6bc9c725ff5e9ea420bf396 Mon Sep 17 00:00:00 2001 From: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> Date: Sat, 6 Jun 2026 21:14:00 +0900 Subject: [PATCH] Apply PropertyNamingStrategy to get/is-prefixed property names The "avoid clobbering get/is names" hack in ModelResolver replaced the Jackson-resolved property name with the raw member name for any member whose name starts with a get/is prefix followed by a lower-case letter. When a custom PropertyNamingStrategy (e.g. SNAKE_CASE) was configured, this clobbered the translated name: record components such as issuanceDate were emitted as "issuanceDate" instead of "issuance_date", while sibling fields like familyName were correctly translated. Only apply the hack when no PropertyNamingStrategy is configured, so the strategy is honored uniformly. The original #415 (Scala is_persistent) and #2635 (JAXB-renamed is-prefixed fields) behavior is preserved, as neither configures a naming strategy. Reported in springdoc/springdoc-openapi#3293. Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com> --- .../v3/core/jackson/ModelResolver.java | 10 +++- .../RecordPropertyNamingStrategyTest.java | 52 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 modules/swagger-java17-support/src/test/java/io/swagger/v3/java17/resolving/RecordPropertyNamingStrategyTest.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..3df6f79fce 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 @@ -678,7 +678,15 @@ public Schema resolve(AnnotatedType annotatedType, ModelConverterContext context // hack to avoid clobbering properties with get/is names // it's ugly but gets around https://github.com/swagger-api/swagger-core/issues/415 - if (propDef.getPrimaryMember() != null) { + // + // This restores the raw member name for members that merely start with a get/is prefix + // (e.g. a Scala accessor "is_persistent", or a JAXB-renamed field, see #415 and #2635). + // It must NOT run when a custom PropertyNamingStrategy (e.g. SNAKE_CASE) is configured: + // in that case the strategy legitimately translates names such as "issuanceDate" -> + // "issuance_date" and the raw member name must not clobber the translated one + // (see springdoc/springdoc-openapi#3293). + if (propDef.getPrimaryMember() != null + && _mapper.getSerializationConfig().getPropertyNamingStrategy() == null) { final JsonProperty jsonPropertyAnn = propDef.getPrimaryMember().getAnnotation(JsonProperty.class); if (jsonPropertyAnn == null || !jsonPropertyAnn.value().equals(propName)) { if (member != null) { diff --git a/modules/swagger-java17-support/src/test/java/io/swagger/v3/java17/resolving/RecordPropertyNamingStrategyTest.java b/modules/swagger-java17-support/src/test/java/io/swagger/v3/java17/resolving/RecordPropertyNamingStrategyTest.java new file mode 100644 index 0000000000..5596beb428 --- /dev/null +++ b/modules/swagger-java17-support/src/test/java/io/swagger/v3/java17/resolving/RecordPropertyNamingStrategyTest.java @@ -0,0 +1,52 @@ +package io.swagger.v3.java17.resolving; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +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.oas.models.media.Schema; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * Reproduces the property-naming inconsistency reported in + * springdoc/springdoc-openapi#3293: when a {@link PropertyNamingStrategies} such as + * {@code SNAKE_CASE} is configured, record components whose names start with the + * {@code is}/{@code get} accessor prefixes followed by a lower-case letter (e.g. + * {@code issuanceDate}) were not being translated, because the + * "avoid clobbering get/is names" hack in {@code ModelResolver} replaced the + * Jackson-resolved (translated) name with the raw component name. + */ +public class RecordPropertyNamingStrategyTest { + + public record PidLookupResponse( + String familyName, + String expiryDate, + String issuanceDate, + String issuingCountry, + String issuingAuthority + ) { + } + + @Test + public void testSnakeCaseNamingStrategyAppliedToRecordComponents() { + ModelResolver modelResolver = new ModelResolver( + Json.mapper().copy().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)); + ModelConverterContextImpl context = new ModelConverterContextImpl(modelResolver); + + Schema schema = modelResolver.resolve(new AnnotatedType(PidLookupResponse.class), context, null); + + assertTrue(schema.getProperties().containsKey("family_name")); + assertTrue(schema.getProperties().containsKey("expiry_date")); + assertTrue(schema.getProperties().containsKey("issuance_date"), + "expected issuance_date but got " + schema.getProperties().keySet()); + assertTrue(schema.getProperties().containsKey("issuing_country"), + "expected issuing_country but got " + schema.getProperties().keySet()); + assertTrue(schema.getProperties().containsKey("issuing_authority"), + "expected issuing_authority but got " + schema.getProperties().keySet()); + assertEquals(schema.getProperties().size(), 5, "unexpected properties: " + schema.getProperties().keySet()); + } +}