fix: global naming strategy not applied to Java records (#7656) - #7657
Conversation
wenshao
left a comment
There was a problem hiding this comment.
[Critical] Deserialization round-trip is broken for records with naming strategies
This PR fixes only the writer side. The reader side (ObjectReaderCreator line ~194) assigns fieldName = paramName from the canonical constructor parameter names with no naming-strategy transformation. After this PR, JSON.toJSONString(new RecordDto("840")) produces {"currency_code":"840"}, but JSON.parseObject("{\"currency_code\":\"840\"}", RecordDto.class) fails to match the key currency_code against the raw parameter name currencyCode — the value is silently dropped.
The existing test only asserts serialization, not round-trip. The reader-side createFieldReaders(provider, objectClass, objectType, owner, parameters, paramNames) overload needs to apply beanInfo.namingStrategy to parameter names, similar to how the field-based overload applies it via BeanUtils.fieldName(fieldName, namingStrategy).
— qwen3.7-max via Qwen Code /review
|
@wenshao @1919chichi As it seems related to the changes, please also ensure code gen code also gets the same fix (if needed). |
|
Thanks for the thorough review! Here's a summary of the changes made to address all three points: 1. 2. Deserialization round-trip (Reader side) 3. JDK 11 compilation failure (Tests) |
|
|
||
| import com.alibaba.fastjson2.JSON; | ||
| import com.alibaba.fastjson2.PropertyNamingStrategy; | ||
| import org.junit.jupiter.api.Tag; |
There was a problem hiding this comment.
[Critical] Missing import causes compilation failure
The import com.alibaba.fastjson2.PropertyNamingStrategy was removed, but the class is still referenced at lines 27, 33, and 55 (PropertyNamingStrategy.SnakeCase). Since this file is in package com.alibaba.fastjson2.annotation and PropertyNamingStrategy lives in com.alibaba.fastjson2, the explicit import is required.
This causes core module test compilation to fail:
JSONTypeNamingSnake.java:[27,24] cannot find symbol
JSONTypeNamingSnake.java:[33,24] cannot find symbol
JSONTypeNamingSnake.java:[55,24] cannot find symbol
| import org.junit.jupiter.api.Tag; | |
| import com.alibaba.fastjson2.JSON; | |
| import com.alibaba.fastjson2.PropertyNamingStrategy; | |
| import org.junit.jupiter.api.Tag; |
— qwen3.7-max via Qwen Code /review
| } | ||
|
|
||
| @Test | ||
| public void testRecordGlobalNaming() { |
There was a problem hiding this comment.
[Suggestion] testRecordGlobalNaming only tests serialization — deserialization with global naming strategy is untested
The reader-side fix in ObjectReaderCreator.java:1282-1286 applies naming strategy to record constructor parameter names when beanInfo.namingStrategy is non-null. For global strategies, this value comes from ObjectReaderProvider via the BeanInfo constructor. This entire code path has zero test coverage.
A regression in reader-side global naming support for records would go undetected.
| public void testRecordGlobalNaming() { | |
| @Test | |
| public void testRecordGlobalNaming() { | |
| ObjectWriterProvider writerProvider = JSONFactory.getDefaultObjectWriterProvider(); | |
| ObjectReaderProvider readerProvider = JSONFactory.getDefaultObjectReaderProvider(); | |
| PropertyNamingStrategy prevWriter = writerProvider.getNamingStrategy(); | |
| PropertyNamingStrategy prevReader = readerProvider.getNamingStrategy(); | |
| try { | |
| writerProvider.setNamingStrategy(PropertyNamingStrategy.SnakeCase); | |
| readerProvider.setNamingStrategy(PropertyNamingStrategy.SnakeCase); | |
| assertEquals("{\"currency_code\":\"840\"}", JSON.toJSONString(new GlobalRecordDto("840"))); | |
| assertEquals(new GlobalRecordDto("840"), JSON.parseObject("{\"currency_code\":\"840\"}", GlobalRecordDto.class)); | |
| } finally { | |
| writerProvider.setNamingStrategy(prevWriter); | |
| readerProvider.setNamingStrategy(prevReader); | |
| } | |
| } |
— qwen3.7-max via Qwen Code /review
| if (fieldInfo.fieldName == null || fieldInfo.fieldName.isEmpty()) { | ||
| if (record) { | ||
| fieldName = method.getName(); | ||
| fieldName = BeanUtils.fieldName(method.getName(), beanInfo.namingStrategy); |
There was a problem hiding this comment.
[Suggestion] Missing null guard for namingStrategy — inconsistent with reader and sibling code
The record branch unconditionally calls BeanUtils.fieldName(method.getName(), beanInfo.namingStrategy). While BeanUtils.fieldName() handles null by defaulting to "CamelCase" (which is a no-op for standard lowercase-starting record names), this is inconsistent with:
- Reader (
ObjectReaderCreator.java:1282):if (beanInfo.namingStrategy != null && parameterNames != null) - Writer field path (line 243):
if (beanInfo.namingStrategy != null)
Adding a null guard makes the code robust against future changes to BeanUtils.fieldName()'s null-handling and keeps the three paths consistent.
| fieldName = BeanUtils.fieldName(method.getName(), beanInfo.namingStrategy); | |
| fieldName = beanInfo.namingStrategy != null | |
| ? BeanUtils.fieldName(method.getName(), beanInfo.namingStrategy) | |
| : method.getName(); |
— qwen3.7-max via Qwen Code /review
|
Updated the PR to address the latest review comments:
Validation run locally:
|
wenshao
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI still running.
The implementation correctly integrates naming strategy support for Java records with proper reader/writer symmetry. @JSONField annotations are correctly resolved via constructor parameter annotations with proper precedence over naming strategy. Test coverage is adequate — serialization, deserialization, boolean prefix handling, and global naming strategy are all verified.
— qwen3.7-max via Qwen Code /review
|
Hi. What's the status of this PR? |
|
CI is all green, but the following issues need to be addressed before merging: 1. Reader-side missing naming strategy support (round-trip broken)
Apply 2.
|
When a record's field name was resolved, `method.getName()` was called directly instead of `BeanUtils.getterName(…, namingStrategy)`, causing the global (and @jsontype) naming strategy to be silently ignored. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Writer: use BeanUtils.fieldName() for records to avoid stripping is/get prefixes that are part of the field name in Java records - Reader: apply namingStrategy to record parameterNames so deserialized JSON keys (e.g. currency_code) match the strategy-applied names - Tests: move record tests to test-jdk17 module (Java 16+ required), replace local record and var with class-level record and explicit types, add round-trip deserialization assertions and boolean prefix test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
33b0511 to
db66664
Compare
Hi All three issues have been addressed in 1. Reader-side missing naming strategy support (round-trip broken)Fixed in if (record && parameterNames == null) {
parameterNames = BeanUtils.getRecordFieldNames(objectClass);
if (beanInfo.namingStrategy != null && parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
parameterNames[i] = BeanUtils.fieldName(parameterNames[i], beanInfo.namingStrategy);
}
}
}The record canonical-constructor parameter names now go through the same naming-strategy transform as the writer side, so a value serialized to Covered by assertEquals("{\"currency_code\":\"840\"}", JSON.toJSONString(new GlobalRecordDto("840")));
assertEquals(new GlobalRecordDto("840"), JSON.parseObject("{\"currency_code\":\"840\"}", GlobalRecordDto.class));2.
|
wenshao
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
— qwen3.8-max-preview via Qwen Code /review
Summary
Fixes #7656
When serializing Java records,
ObjectWriterCreatorwas callingmethod.getName()directly to get the field name, bypassingBeanUtils.getterName(method, kotlin, namingStrategy). This meant that both the global naming strategy (set viaJSONFactory.getDefaultObjectWriterProvider().setNamingStrategy(...)) and the per-type@JSONType(naming = ...)annotation were silently ignored for record components.Root cause (
ObjectWriterCreator.java):Test plan
JSONTypeNamingSnake#testRecord— verifies@JSONType(naming = SnakeCase)applies to recordsJSONTypeNamingSnake#testRecordGlobalNaming— verifies globalsetNamingStrategy(SnakeCase)applies to records./mvnw -pl core clean test— all tests pass./mvnw -pl core -Dfastjson2.creator=reflect clean test— reflect mode tests pass./mvnw -pl core validate— Checkstyle passes