Skip to content

Honor PropertyNamingStrategy for is-prefixed record components#5215

Closed
seonwooj0810 wants to merge 1 commit into
swagger-api:masterfrom
seonwooj0810:fix/3293-snakecase-is-prefix-record
Closed

Honor PropertyNamingStrategy for is-prefixed record components#5215
seonwooj0810 wants to merge 1 commit into
swagger-api:masterfrom
seonwooj0810:fix/3293-snakecase-is-prefix-record

Conversation

@seonwooj0810

Copy link
Copy Markdown

Problem

When a Jackson PropertyNamingStrategy (e.g. SNAKE_CASE) is configured on the ObjectMapper used by ModelResolver, non-boolean record components whose name starts with is are left in camelCase in the generated schema, while every other field is translated correctly.

Given:

public record PidLookupResponse(
        String familyName, String expiryDate,
        String issuanceDate, String issuingCountry, String issuingAuthority) {}

with SNAKE_CASE, the resolved schema contains family_name, expiry_date — but issuanceDate, issuingCountry, issuingAuthority (untranslated).

This was diagnosed as a swagger-core issue by the springdoc-openapi maintainer in springdoc/springdoc-openapi#3293 (reproducible directly against ModelResolver, no Spring involved).

Root cause

The get/is name-clobbering hack (added for #415) in ModelResolver falls back to the raw member name when an accessor starts with get/is followed by a lowercase letter:

if (altName.startsWith(prefix) && length > offset
        && !Character.isUpperCase(altName.charAt(offset))) {
    propName = altName;
}

For a record, the accessor name equals the component name (issuanceDate), so "is" + lowercase matches and propName is overwritten with the camelCase member name — discarding the strategy-translated value that Jackson already computed in propDef.getName(). Classic getters (getIssuanceDate/isActive) are unaffected because the character after the prefix is uppercase.

Fix

Skip the hack when a PropertyNamingStrategy is configured. In that case propDef.getName() is already authoritative and must not be overridden. Default (no-strategy) behavior is unchanged, so the #415 workaround is fully preserved.

if (propDef.getPrimaryMember() != null
        && _mapper.getSerializationConfig().getPropertyNamingStrategy() == null) {

Tests

Added Ticket3293Test in the swagger-java17-support module (records require Java 17). It resolves the record above with a SNAKE_CASE ModelResolver and asserts all five string components plus a genuine boolean component (isActiveis_active) are snake_cased.

Verified the test fails without the fix (the three is* components leak as issuanceDate/issuingCountry/issuingAuthority) and passes with it, with is_active correct in both runs (no boolean-getter regression). Full swagger-java17-support module and the io.swagger.v3.core.resolving.* package pass.

Refs springdoc/springdoc-openapi#3293

The get/is name-clobbering hack (issue swagger-api#415) overwrites the
strategy-translated property name with the raw member name when an
accessor starts with "is" followed by a lowercase letter. For records
the accessor name equals the component name, so non-boolean components
such as issuanceDate / issuingCountry / issuingAuthority were left in
camelCase under a SNAKE_CASE (or any) PropertyNamingStrategy, while
other fields were translated correctly.

Skip the hack when a PropertyNamingStrategy is configured: in that case
propDef.getName() already reflects the strategy and must not be
overridden. Default (no-strategy) behavior is unchanged, so the swagger-api#415
workaround is preserved.

Signed-off-by: seonwoo_jung <79202163+seonwooj0810@users.noreply.github.com>
@Mattias-Sehlstedt

Copy link
Copy Markdown
Contributor

Hi, first I would like to clarify that I am not the maintainer of springdoc, only a contributor. So take everything I say with a grain of salt.

Second, this looks like to be a duplicate of your previous PR #5192?

@seonwooj0810

Copy link
Copy Markdown
Author

You're right, thanks for catching this @Mattias-Sehlstedt. This does overlap with my earlier #5192, which targets the same ModelResolver get/is name-clobbering hack but more broadly (both get- and is-prefixed names, not just is-prefixed record components). To avoid two competing PRs for the same fix I'll close this one in favor of #5192. Apologies for the duplicate.

@seonwooj0810

Copy link
Copy Markdown
Author

Closing as a duplicate of #5192, which addresses the same root cause more comprehensively.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants