diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x
index e65fabf2f4..a48dd549e4 100644
--- a/release-notes/VERSION-2.x
+++ b/release-notes/VERSION-2.x
@@ -18,6 +18,8 @@ Project: jackson-databind
serialization
#5151: Add new exception type, `MissingInjectValueException`, to be used
for failed `@JacksonInject`
+#5152: Support "iPhone" style capitalized properties (add
+ `MapperFeature.FIX_FIELD_NAME_CASE_MISMATCH`)
#5179: Add "current token" info into `MismatchedInputException`
#5192: Record types are broken on Android when using R8
(reported by @HelloOO7)
diff --git a/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java b/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java
index 3a053193c8..9040f7abe7 100644
--- a/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java
+++ b/src/main/java/com/fasterxml/jackson/databind/MapperFeature.java
@@ -218,6 +218,17 @@ public enum MapperFeature implements ConfigFeature
*/
INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES(true),
+ /**
+ * Feature that when enabled will allow getters with is-Prefix also for
+ * non-boolean return types; if disabled only methods that return
+ * {@code boolean} or {@code Boolean} qualify as "is getters".
+ *
+ * Feature is disabled by default for backwards compatibility.
+ *
+ * @since 2.14
+ */
+ ALLOW_IS_GETTERS_FOR_NON_BOOLEAN(false),
+
/**
* Feature that determines whether nominal property type of {@link Void} is
* allowed for Getter methods to indicate {@code null} valued pseudo-property
@@ -541,15 +552,20 @@ public enum MapperFeature implements ConfigFeature
ALLOW_EXPLICIT_PROPERTY_RENAMING(false),
/**
- * Feature that when enabled will allow getters with is-Prefix also for
- * non-boolean return types; if disabled only methods that return
- * {@code boolean} or {@code Boolean} qualify as "is getters".
+ * Feature that can be enabled to solve problem where an upper-case letter in
+ * the first 2 characters of Java field name (like {@code "IPhone"} or {@code "iPhone"})
+ * prevents match with property name derived from accessors (setter like
+ * {@code getIPhone()} becomes {@code "iphone"}).
+ * If enabled, additional checking is done with case-insensitive comparison (for
+ * cases of the first or second letter of Field name being upper-case) to merge
+ * accessors. If disabled, no special processing is done.
*
- * Feature is disabled by default for backwards compatibility.
+ * Feature is disabled by default in 2.x for backwards-compatibility.
+ * It will be enabled by default in 3.0.
*
- * @since 2.14
+ * @since 2.20
*/
- ALLOW_IS_GETTERS_FOR_NON_BOOLEAN(false),
+ FIX_FIELD_NAME_CASE_MISMATCH(false),
/*
/******************************************************
diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java
index 8ad07a53a8..851227bc6e 100644
--- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java
+++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertiesCollector.java
@@ -433,12 +433,11 @@ protected void collectAll()
_potentialCreators = new PotentialCreators();
// First: gather basic accessors
- LinkedHashMap props = new LinkedHashMap();
+ LinkedHashMap props = new LinkedHashMap<>();
// 14-Nov-2024, tatu: Previously skipped checking fields for Records; with 2.18+ won't
// (see [databind#3628], [databind#3895], [databind#3992], [databind#4626])
_addFields(props); // note: populates _fieldRenameMappings
-
_addMethods(props);
// 25-Jan-2016, tatu: Avoid introspecting (constructor-)creators for non-static
// inner classes, see [databind#1502]
@@ -446,7 +445,11 @@ protected void collectAll()
if (!_classDef.isNonStaticInnerClass()) {
_addCreators(props);
}
-
+ // 11-Jun-2025, tatu: [databind#5152] May need to "fix" mis-matching leading case
+ // wrt Fields vs Accessors
+ if (_config.isEnabled(MapperFeature.FIX_FIELD_NAME_CASE_MISMATCH)) {
+ _fixLeadingFieldNameCase(props);
+ }
// Remove ignored properties, first; this MUST precede annotation merging
// since logic relies on knowing exactly which accessor has which annotation
_removeUnwantedProperties(props);
@@ -547,10 +550,9 @@ private Map _putAnyGettersInTheEnd(
protected void _addFields(Map props)
{
final AnnotationIntrospector ai = _annotationIntrospector;
- /* 28-Mar-2013, tatu: For deserialization we may also want to remove
- * final fields, as often they won't make very good mutators...
- * (although, maybe surprisingly, JVM _can_ force setting of such fields!)
- */
+ // 28-Mar-2013, tatu: For deserialization we may also want to remove
+ // final fields, as often they won't make very good mutators...
+ // (although, maybe surprisingly, JVM _can_ force setting of such fields!)
final boolean pruneFinalFields = !_forSerialization && !_config.isEnabled(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS);
final boolean transientAsIgnoral = _config.isEnabled(MapperFeature.PROPAGATE_TRANSIENT_MARKER);
@@ -1318,6 +1320,97 @@ private String _checkRenameByField(String implName) {
return implName;
}
+ /*
+ /**********************************************************
+ /* Internal methods; merging/fixing case-differences
+ /**********************************************************
+ */
+
+ // @since 2.20
+ protected void _fixLeadingFieldNameCase(Map props)
+ {
+ // 11-Jun-2025, tatu: [databind#5152] May need to "fix" mis-matching leading case
+ // wrt Fields vs Accessors
+
+ // First: find possible candidates where:
+ //
+ // 1. Property only has Field
+ // 2. Field does NOT have explicit name (renaming)
+ // 3. Implicit name has upper-case for first and/or second character
+
+ Map fieldsToCheck = null;
+ for (Map.Entry entry : props.entrySet()) {
+ POJOPropertyBuilder prop = entry.getValue();
+
+ // First: (1) and (2)
+ if (!prop.hasFieldAndNothingElse()
+ || prop.isExplicitlyNamed()) {
+ continue;
+ }
+ // Second: (3)
+ if (!_firstOrSecondCharUpperCase(entry.getKey())) {
+ continue;
+ }
+ if (fieldsToCheck == null) {
+ fieldsToCheck = new HashMap<>();
+ }
+ fieldsToCheck.put(entry.getKey(), prop);
+ }
+ /*// DEBUGGING
+ if (fieldsToCheck == null) {
+ System.err.println("_fixLeadingCase, candidates -> null; props -> "+props.keySet());
+ } else {
+ System.err.println("_fixLeadingCase, candidates -> "+fieldsToCheck);
+ }
+ */
+
+ if (fieldsToCheck == null) {
+ return;
+ }
+
+ for (Map.Entry fieldEntry : fieldsToCheck.entrySet()) {
+ Iterator> it = props.entrySet().iterator();
+ final POJOPropertyBuilder fieldProp = fieldEntry.getValue();
+ final String fieldName = fieldEntry.getKey();
+
+ while (it.hasNext()) {
+ Map.Entry propEntry = it.next();
+ final POJOPropertyBuilder prop = propEntry.getValue();
+
+ // Skip anything that has Field (can't merge)
+ if (prop == fieldProp || prop.hasField()) {
+ continue;
+ }
+ if (fieldName.equalsIgnoreCase(propEntry.getKey())) {
+ // Remove non-Field property; add its accessors to Field one
+ it.remove();
+ fieldProp.addAll(prop);
+ // Should we continue with possible other accessors?
+ // For now assume only one merge needed/desired
+ break;
+ }
+ }
+ }
+ }
+
+ // @since 2.20
+ private boolean _firstOrSecondCharUpperCase(String name) {
+ switch (name.length()) {
+ case 0:
+ return false;
+ default:
+ if (!Character.isLowerCase(name.charAt(1))) {
+ return true;
+ }
+ // fall through
+ case 1:
+ if (!Character.isLowerCase(name.charAt(0))) {
+ return true;
+ }
+ return false;
+ }
+ }
+
/*
/**********************************************************
/* Internal methods; removing ignored properties
@@ -1420,6 +1513,7 @@ protected void _renameProperties(Map props)
// With renaming need to do in phases: first, find properties to rename
Iterator> it = props.entrySet().iterator();
LinkedList renamed = null;
+
while (it.hasNext()) {
Map.Entry entry = it.next();
POJOPropertyBuilder prop = entry.getValue();
diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertyBuilder.java b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertyBuilder.java
index 088a7f8b30..4dca05c49c 100644
--- a/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertyBuilder.java
+++ b/src/main/java/com/fasterxml/jackson/databind/introspect/POJOPropertyBuilder.java
@@ -385,6 +385,12 @@ public Class> getRawPrimaryType() {
@Override
public boolean hasField() { return _fields != null; }
+ // @since 2.20 additional accessor
+ public boolean hasFieldAndNothingElse() {
+ return (_fields != null)
+ && ((_getters == null) && (_setters == null) && (_ctorParameters == null));
+ }
+
@Override
public boolean hasConstructorParameter() { return _ctorParameters != null; }
@@ -416,9 +422,8 @@ public AnnotatedMethod getGetter()
}
// But if multiple, verify that they do not conflict...
for (; next != null; next = next.next) {
- /* [JACKSON-255] Allow masking, i.e. do not report exception if one
- * is in super-class from the other
- */
+ // Allow masking, i.e. do not report exception if one
+ // is in super-class from the other
Class> currClass = curr.value.getDeclaringClass();
Class> nextClass = next.value.getDeclaringClass();
if (currClass != nextClass) {
diff --git a/src/test/java/com/fasterxml/jackson/databind/tofix/IPhoneStyleProperty5152Test.java b/src/test/java/com/fasterxml/jackson/databind/misc/IPhoneStyleProperty5152Test.java
similarity index 83%
rename from src/test/java/com/fasterxml/jackson/databind/tofix/IPhoneStyleProperty5152Test.java
rename to src/test/java/com/fasterxml/jackson/databind/misc/IPhoneStyleProperty5152Test.java
index 5ca116728c..7e1125fba8 100644
--- a/src/test/java/com/fasterxml/jackson/databind/tofix/IPhoneStyleProperty5152Test.java
+++ b/src/test/java/com/fasterxml/jackson/databind/misc/IPhoneStyleProperty5152Test.java
@@ -1,11 +1,9 @@
-package com.fasterxml.jackson.databind.tofix;
+package com.fasterxml.jackson.databind.misc;
import org.junit.jupiter.api.Test;
-import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
-import com.fasterxml.jackson.databind.testutil.failure.JacksonTestFailureExpected;
import static org.junit.jupiter.api.Assertions.*;
@@ -73,10 +71,27 @@ public void setPhone(String value) {
}
}
+ // [databind#2696]
+ static class OAuthTokenBean {
+ protected String oAuthToken;
+
+ public OAuthTokenBean(String t) {
+ oAuthToken = t;
+ }
+
+ public String getOAuthToken() {
+ return this.oAuthToken;
+ }
+
+ public void setOAuthToken(String oAuthToken) {
+ this.oAuthToken = oAuthToken;
+ }
+ }
+
private final ObjectMapper MAPPER = jsonMapperBuilder()
+ .enable(MapperFeature.FIX_FIELD_NAME_CASE_MISMATCH)
.build();
- @JacksonTestFailureExpected
@Test
public void testIPhoneStyleProperty() throws Exception {
// Test with iPhone style property
@@ -104,21 +119,19 @@ public void testRegularPojoProperty() throws Exception {
}
// [databind#2835]: "dLogHeader" property
- @JacksonTestFailureExpected
@Test
public void testDLogHeaderStyleProperty() throws Exception {
// Test with DLogHeader style property
- String json = "{\"dLogHeader\":\"Debug Log Header\"}";
+ String json = "{\"DLogHeader\":\"Debug Log Header\"}";
DLogHeaderBean result = MAPPER.readValue(json, DLogHeaderBean.class);
assertNotNull(result);
assertEquals("Debug Log Header", result.getDLogHeader());
// Test serialization
String serialized = MAPPER.writeValueAsString(result);
- assertEquals("{\"dLogHeader\":\"Debug Log Header\"}", serialized);
+ assertEquals("{\"DLogHeader\":\"Debug Log Header\"}", serialized);
}
- @JacksonTestFailureExpected
@Test
public void testKBSBroadCastingStyleProperty() throws Exception {
// Test with KBSBroadCasting style property
@@ -132,7 +145,6 @@ public void testKBSBroadCastingStyleProperty() throws Exception {
assertEquals("{\"KBSBroadCasting\":\"Korean Broadcasting System\"}", serialized);
}
- @JacksonTestFailureExpected
@Test
public void testPhoneStyleProperty() throws Exception {
// Test with Phone style property
@@ -146,4 +158,10 @@ public void testPhoneStyleProperty() throws Exception {
assertEquals("{\"Phone\":\"iPhone 15\"}", serialized);
}
-}
\ No newline at end of file
+ // [databind#2696]
+ @Test
+ public void testOAuthProperty() throws Exception {
+ assertEquals(a2q("{'oAuthToken':'123'}"),
+ MAPPER.writeValueAsString(new OAuthTokenBean("123")));
+ }
+}