Skip to content

Commit dcb2ac6

Browse files
committed
Support 5152
5152 Add more Capital case possible 5152 Add new MapperFeature 5152 Move handling to separate method Finalize 5152 Add nameValidator check also Fix test cases Add more test case fix-5152 Fix iPhone style
1 parent c3b7946 commit dcb2ac6

File tree

3 files changed

+320
-9
lines changed

3 files changed

+320
-9
lines changed

src/main/java/com/fasterxml/jackson/databind/MapperFeature.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,38 @@ public enum MapperFeature implements ConfigFeature
529529
*/
530530
USE_STD_BEAN_NAMING(false),
531531

532+
/**
533+
* Feature that determines how to handle property names with mixed capitalization patterns.
534+
* This feature specifically addresses cases where getter/setter methods have names that
535+
* start with uppercase letters followed by more uppercase letters.
536+
* <p>
537+
* When enabled, the following rules apply:
538+
* <ul>
539+
* <li>For single-letter uppercase prefixes followed by uppercase (like "getIPhone"):
540+
* the first letter is lowercased ("iPhone")</li>
541+
* <li>For multiple uppercase letters at start (like "getKBSBroadCasting" or "getURL"):
542+
* the original case is preserved ("KBSBroadCasting", "URL")</li>
543+
* </ul>
544+
* <p>
545+
* When enabled, overwrites {@link #USE_STD_BEAN_NAMING}.
546+
* When disabled, Jackson's standard property naming rules apply using {@link #USE_STD_BEAN_NAMING}.
547+
* <p>
548+
* Examples with this feature enabled:
549+
* <ul>
550+
* <li>"getIPhone" → "iPhone" (single uppercase + uppercase)</li>
551+
* <li>"getDLogHeader" → "dLogHeader" (single uppercase + uppercase)</li>
552+
* <li>"getKBSBroadCasting" → "KBSBroadCasting" (multiple uppercase)</li>
553+
* <li>"getURL" → "URL" (multiple uppercase)</li>
554+
* <li>"getValue" → "value" (standard case, no special handling)</li>
555+
* </ul>
556+
* <p>
557+
* This feature is disabled by default for backward compatibility in Jackson 2.x version and
558+
* will be enabled by default in Jackson 3.0 version.
559+
*
560+
* @since 2.20
561+
*/
562+
MIXED_CAPS_PROPERTY_NAMING(false),
563+
532564
/**
533565
* Feature that when enabled will allow explicitly named properties (i.e., fields or methods
534566
* annotated with {@link com.fasterxml.jackson.annotation.JsonProperty}("explicitName")) to

src/main/java/com/fasterxml/jackson/databind/introspect/DefaultAccessorNamingStrategy.java

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,29 @@ protected DefaultAccessorNamingStrategy(MapperConfig<?> config, AnnotatedClass f
7373
_baseNameValidator = baseNameValidator;
7474
}
7575

76+
/**
77+
* Common method to handle property name mangling for all accessor types.
78+
* First checks for mixed caps handling if enabled, then falls back to standard or legacy handling.
79+
*/
80+
private String handlePropertyName(String name, int prefixLength) {
81+
if (_config.isEnabled(MapperFeature.MIXED_CAPS_PROPERTY_NAMING)) {
82+
String result = mixedCapsManglePropertyName(name, prefixLength);
83+
if (result != null) {
84+
return result;
85+
}
86+
}
87+
return _stdBeanNaming
88+
? stdManglePropertyName(name, prefixLength)
89+
: legacyManglePropertyName(name, prefixLength);
90+
}
91+
7692
@Override
7793
public String findNameForIsGetter(AnnotatedMethod am, String name)
7894
{
7995
if (_isGetterPrefix != null) {
8096
if (_isGettersNonBoolean || _booleanType(am.getType())) {
8197
if (name.startsWith(_isGetterPrefix)) {
82-
return _stdBeanNaming
83-
? stdManglePropertyName(name, _isGetterPrefix.length())
84-
: legacyManglePropertyName(name, _isGetterPrefix.length());
98+
return handlePropertyName(name, _isGetterPrefix.length());
8599
}
86100
}
87101
}
@@ -121,9 +135,7 @@ public String findNameForRegularGetter(AnnotatedMethod am, String name)
121135
return null;
122136
}
123137
}
124-
return _stdBeanNaming
125-
? stdManglePropertyName(name, _getterPrefix.length())
126-
: legacyManglePropertyName(name, _getterPrefix.length());
138+
return handlePropertyName(name, _getterPrefix.length());
127139
}
128140
return null;
129141
}
@@ -132,9 +144,7 @@ public String findNameForRegularGetter(AnnotatedMethod am, String name)
132144
public String findNameForMutator(AnnotatedMethod am, String name)
133145
{
134146
if ((_mutatorPrefix != null) && name.startsWith(_mutatorPrefix)) {
135-
return _stdBeanNaming
136-
? stdManglePropertyName(name, _mutatorPrefix.length())
137-
: legacyManglePropertyName(name, _mutatorPrefix.length());
147+
return handlePropertyName(name, _mutatorPrefix.length());
138148
}
139149
return null;
140150
}
@@ -179,6 +189,7 @@ protected String legacyManglePropertyName(final String basename, final int offse
179189
if (c == d) {
180190
return basename.substring(offset);
181191
}
192+
182193
// otherwise, lower case initial chars. Common case first, just one char
183194
StringBuilder sb = new StringBuilder(end - offset);
184195
sb.append(d);
@@ -215,6 +226,7 @@ protected String stdManglePropertyName(final String basename, final int offset)
215226
if (c0 == c1) {
216227
return basename.substring(offset);
217228
}
229+
218230
// 17-Dec-2014, tatu: As per [databind#653], need to follow more
219231
// closely Java Beans spec; specifically, if two first are upper-case,
220232
// then no lower-casing should be done.
@@ -229,6 +241,58 @@ protected String stdManglePropertyName(final String basename, final int offset)
229241
return sb.toString();
230242
}
231243

244+
/**
245+
* Method that handles mixed caps property naming according to the rules defined in
246+
* {@link MapperFeature#MIXED_CAPS_PROPERTY_NAMING}.
247+
*
248+
* @since 2.20
249+
*/
250+
protected String mixedCapsManglePropertyName(final String basename, final int offset)
251+
{
252+
final int end = basename.length();
253+
if (end == offset) { // empty name, nope
254+
return null;
255+
}
256+
char c = basename.charAt(offset);
257+
// 12-Oct-2020, tatu: Additional configurability; allow checking that
258+
// base name is acceptable (currently just by checking first character)
259+
if (_baseNameValidator != null) {
260+
if (!_baseNameValidator.accept(c, basename, offset)) {
261+
return null;
262+
}
263+
}
264+
265+
// next check: is the first character upper case? If not, return as is
266+
char d = Character.toLowerCase(c);
267+
268+
if (c == d) {
269+
return basename.substring(offset);
270+
}
271+
272+
if (offset + 1 < end) {
273+
char nextChar = basename.charAt(offset + 1);
274+
if (Character.isUpperCase(nextChar)) {
275+
// Count how many uppercase letters we have in a row
276+
int upperCount = 2; // We already know first two are uppercase
277+
while (offset + upperCount < end && Character.isUpperCase(basename.charAt(offset + upperCount))) {
278+
upperCount++;
279+
}
280+
281+
// If we have more than 2 uppercase letters in a row, preserve the original case
282+
if (upperCount > 2) {
283+
return basename.substring(offset);
284+
}
285+
286+
// This is a case like IPhone - lowercase first character
287+
StringBuilder sb = new StringBuilder(end - offset);
288+
sb.append(d);
289+
sb.append(basename.substring(offset + 1)); // Keep the rest as is
290+
return sb.toString();
291+
}
292+
}
293+
return null;
294+
}
295+
232296
/*
233297
/**********************************************************************
234298
/* Legacy methods moved in 2.12 from "BeanUtil" -- are these still needed?
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package com.fasterxml.jackson.databind.introspect;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
6+
import com.fasterxml.jackson.databind.*;
7+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
8+
9+
import static org.junit.jupiter.api.Assertions.*;
10+
11+
// [databind#5152] Support "iPhone" style capitalized properties
12+
public class IPhoneStyleProperty5152Test
13+
extends DatabindTestUtil
14+
{
15+
static class IPhoneBean {
16+
private String iPhone;
17+
18+
public String getIPhone() {
19+
return iPhone;
20+
}
21+
22+
public void setIPhone(String value) {
23+
iPhone = value;
24+
}
25+
}
26+
27+
static class RegularBean {
28+
private String phoneNumber;
29+
30+
public String getPhoneNumber() {
31+
return phoneNumber;
32+
}
33+
34+
public void setPhoneNumber(String value) {
35+
phoneNumber = value;
36+
}
37+
}
38+
39+
40+
static class DLogHeaderBean {
41+
private String DLogHeader;
42+
43+
public String getDLogHeader() {
44+
return DLogHeader;
45+
}
46+
47+
public void setDLogHeader(String value) {
48+
DLogHeader = value;
49+
}
50+
}
51+
52+
static class KBSBroadCastingBean {
53+
private String KBSBroadCasting;
54+
55+
public String getKBSBroadCasting() {
56+
return KBSBroadCasting;
57+
}
58+
59+
public void setKBSBroadCasting(String value) {
60+
KBSBroadCasting = value;
61+
}
62+
}
63+
64+
static class PhoneBean {
65+
private String Phone;
66+
67+
public String getPhone() {
68+
return Phone;
69+
}
70+
public void setPhone(String value) {
71+
Phone = value;
72+
}
73+
}
74+
75+
@JsonPropertyOrder({ "4Roses", "$dollar", "_underscore" })
76+
static class NonLetterFirstCharBean {
77+
private String _4Roses;
78+
private String $dollar;
79+
private String _underscore;
80+
81+
public String get4Roses() {
82+
return _4Roses;
83+
}
84+
85+
public void set4Roses(String value) {
86+
_4Roses = value;
87+
}
88+
89+
public String get$dollar() {
90+
return $dollar;
91+
}
92+
93+
public void set$dollar(String value) {
94+
$dollar = value;
95+
}
96+
97+
public String get_underscore() {
98+
return _underscore;
99+
}
100+
101+
public void set_underscore(String value) {
102+
_underscore = value;
103+
}
104+
}
105+
106+
private final ObjectMapper ENABLED = jsonMapperBuilder()
107+
.enable(MapperFeature.MIXED_CAPS_PROPERTY_NAMING)
108+
.build();
109+
110+
private final ObjectMapper ENABLED_WITH_VALIDATION = jsonMapperBuilder()
111+
.enable(MapperFeature.MIXED_CAPS_PROPERTY_NAMING)
112+
.accessorNaming(new DefaultAccessorNamingStrategy.Provider()
113+
.withFirstCharAcceptance(false, false)) // Don't allow lowercase or non-letter first chars
114+
.build();
115+
116+
@Test
117+
public void testIPhoneStyleProperty() throws Exception {
118+
// Test with iPhone style property
119+
String json = "{\"iPhone\":\"iPhone 15\"}";
120+
IPhoneBean result = ENABLED.readValue(json, IPhoneBean.class);
121+
assertNotNull(result);
122+
assertEquals("iPhone 15", result.getIPhone());
123+
124+
// Test serialization
125+
String serialized = ENABLED.writeValueAsString(result);
126+
assertEquals("{\"iPhone\":\"iPhone 15\"}", serialized);
127+
}
128+
129+
@Test
130+
public void testRegularPojoProperty() throws Exception {
131+
// Test with regular POJO property
132+
String json = "{\"phoneNumber\":\"123-456-7890\"}";
133+
RegularBean result = ENABLED.readValue(json, RegularBean.class);
134+
assertNotNull(result);
135+
assertEquals("123-456-7890", result.getPhoneNumber());
136+
137+
// Test serialization
138+
String serialized = ENABLED.writeValueAsString(result);
139+
assertEquals("{\"phoneNumber\":\"123-456-7890\"}", serialized);
140+
}
141+
142+
143+
@Test
144+
public void testDLogHeaderStyleProperty() throws Exception {
145+
// Test with DLogHeader style property
146+
String json = "{\"dLogHeader\":\"Debug Log Header\"}";
147+
DLogHeaderBean result = ENABLED.readValue(json, DLogHeaderBean.class);
148+
assertNotNull(result);
149+
assertEquals("Debug Log Header", result.getDLogHeader());
150+
151+
// Test serialization
152+
String serialized = ENABLED.writeValueAsString(result);
153+
assertEquals("{\"dLogHeader\":\"Debug Log Header\"}", serialized);
154+
}
155+
156+
@Test
157+
public void testKBSBroadCastingStyleProperty() throws Exception {
158+
// Test with KBSBroadCasting style property
159+
String json = "{\"KBSBroadCasting\":\"Korean Broadcasting System\"}";
160+
KBSBroadCastingBean result = ENABLED.readValue(json, KBSBroadCastingBean.class);
161+
assertNotNull(result);
162+
assertEquals("Korean Broadcasting System", result.getKBSBroadCasting());
163+
164+
// Test serialization
165+
String serialized = ENABLED.writeValueAsString(result);
166+
assertEquals("{\"KBSBroadCasting\":\"Korean Broadcasting System\"}", serialized);
167+
}
168+
169+
@Test
170+
public void testNonLetterFirstCharWithValidation() throws Exception {
171+
// Test with validation enabled - should ignore properties starting with non-letters
172+
NonLetterFirstCharBean result = ENABLED_WITH_VALIDATION.reader()
173+
.without(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
174+
.readValue("{\"4Roses\":\"Four Roses\",\"$dollar\":\"Dollar\",\"_underscore\":\"Underscore\"}",
175+
NonLetterFirstCharBean.class);
176+
assertNotNull(result);
177+
assertNull(result.get4Roses());
178+
assertNull(result.get$dollar());
179+
assertNull(result.get_underscore());
180+
181+
// Test serialization - should not include properties starting with non-letters
182+
String serialized = ENABLED_WITH_VALIDATION.writeValueAsString(result);
183+
assertEquals("{}", serialized);
184+
}
185+
186+
@Test
187+
public void testNonLetterFirstCharWithoutValidation() throws Exception {
188+
// Test without validation - should accept properties starting with non-letters
189+
NonLetterFirstCharBean result = ENABLED.readValue(
190+
"{\"4Roses\":\"Four Roses\",\"$dollar\":\"Dollar\",\"_underscore\":\"Underscore\"}",
191+
NonLetterFirstCharBean.class);
192+
assertNotNull(result);
193+
assertEquals("Four Roses", result.get4Roses());
194+
assertEquals("Dollar", result.get$dollar());
195+
assertEquals("Underscore", result.get_underscore());
196+
197+
// Test serialization
198+
String serialized = ENABLED.writeValueAsString(result);
199+
assertEquals("{\"4Roses\":\"Four Roses\",\"$dollar\":\"Dollar\",\"_underscore\":\"Underscore\"}", serialized);
200+
}
201+
202+
@Test
203+
public void testPhoneStyleProperty() throws Exception {
204+
// Test with Phone style property
205+
String json = "{\"Phone\":\"iPhone 15\"}";
206+
PhoneBean result = ENABLED.readValue(json, PhoneBean.class);
207+
assertNotNull(result);
208+
assertEquals("iPhone 15", result.getPhone());
209+
210+
// Test serialization
211+
String serialized = ENABLED.writeValueAsString(result);
212+
assertEquals("{\"Phone\":\"iPhone 15\"}", serialized);
213+
}
214+
215+
}

0 commit comments

Comments
 (0)