Repository: googleapis/google-http-java-client
Pull request #1831 (2023-03-11 04:09:49)

fix: JSON deserialization setter case priority
Fixes the problem of JSON deserialization ignoring the case of setters. Now case-sensitive setter method matches are considered first.

Fixes: #1830.

suztomo: I didn’t know this case priority behavior. If not done yet, can you write down the desired behavior about the setter case priority in the Javadoc? Include how it helps the users of this HTTP client library.

If the current implementation was not doing the job in the spec, then this PR is a bug fix.
meltsufin: > I didn’t know this case priority behavior. If not done yet, can you write down the desired behavior about the setter case priority in the Javadoc? Include how it helps the users of this HTTP client library.
> 
> If the current implementation was not doing the job in the spec, then this PR is a bug fix.

It's a bugfix. Internal ref: b/267807198.

Patch:
diff --git a/google-http-client/src/main/java/com/google/api/client/util/FieldInfo.java b/google-http-client/src/main/java/com/google/api/client/util/FieldInfo.java
index 3830a866..36d0dc9d 100644
--- a/google-http-client/src/main/java/com/google/api/client/util/FieldInfo.java
+++ b/google-http-client/src/main/java/com/google/api/client/util/FieldInfo.java
@@ -134,12 +134,20 @@ public class FieldInfo {
   }
 
   /** Creates list of setter methods for a field only in declaring class. */
-  private Method[] settersMethodForField(Field field) {
+  private Method[] settersMethodForField(final Field field) {
     List<Method> methods = new ArrayList<>();
+    String fieldSetter = "set" + Ascii.toUpperCase(field.getName().substring(0, 1));
+    if (field.getName().length() > 1) {
+      fieldSetter += field.getName().substring(1);
+    }
     for (Method method : field.getDeclaringClass().getDeclaredMethods()) {
-      if (Ascii.toLowerCase(method.getName()).equals("set" + Ascii.toLowerCase(field.getName()))
-          && method.getParameterTypes().length == 1) {
-        methods.add(method);
+      if (method.getParameterTypes().length == 1) {
+        // add case-sensitive matches first in the list
+        if (method.getName().equals(fieldSetter)) {
+          methods.add(0, method);
+        } else if (Ascii.toLowerCase(method.getName()).equals(Ascii.toLowerCase(fieldSetter))) {
+          methods.add(method);
+        }
       }
     }
     return methods.toArray(new Method[0]);
@@ -216,15 +224,13 @@ public class FieldInfo {
    * value.
    */
   public void setValue(Object obj, Object value) {
-    if (setters.length > 0) {
-      for (Method method : setters) {
-        if (value == null || method.getParameterTypes()[0].isAssignableFrom(value.getClass())) {
-          try {
-            method.invoke(obj, value);
-            return;
-          } catch (IllegalAccessException | InvocationTargetException e) {
-            // try to set field directly
-          }
+    for (Method method : setters) {
+      if (value == null || method.getParameterTypes()[0].isAssignableFrom(value.getClass())) {
+        try {
+          method.invoke(obj, value);
+          return;
+        } catch (IllegalAccessException | InvocationTargetException e) {
+          // try to set field directly
         }
       }
     }


Test patch:
diff --git a/google-http-client/src/test/java/com/google/api/client/util/FieldInfoTest.java b/google-http-client/src/test/java/com/google/api/client/util/FieldInfoTest.java
index c000a90d..0cd1c17d 100644
--- a/google-http-client/src/test/java/com/google/api/client/util/FieldInfoTest.java
+++ b/google-http-client/src/test/java/com/google/api/client/util/FieldInfoTest.java
@@ -14,6 +14,7 @@
 
 package com.google.api.client.util;
 
+import com.google.api.client.json.GenericJson;
 import junit.framework.TestCase;
 
 /**
@@ -49,4 +50,36 @@ public class FieldInfoTest extends TestCase {
     assertEquals(E.OTHER_VALUE, FieldInfo.of(E.OTHER_VALUE).<E>enumValue());
     assertEquals(E.NULL, FieldInfo.of(E.NULL).<E>enumValue());
   }
+
+  public static final class Data extends GenericJson {
+    @Key String passcode;
+    @Key String passCode;
+
+    public Data setPasscode(String passcode) {
+      this.passcode = passcode;
+      return this;
+    }
+
+    public Data setPassCode(String passCode) {
+      this.passCode = passCode;
+      return this;
+    }
+  }
+
+  public void testSetValueCaseSensitivityPriority() {
+    Data data = new Data();
+    data.setPasscode("pass1");
+    data.setPassCode("pass2");
+    data.set("passCode", "passX");
+
+    assertEquals(data.passcode, "pass1");
+    assertEquals(data.passCode, "passX");
+
+    data.setPasscode("pass1");
+    data.setPassCode("pass2");
+    data.set("passcode", "passX");
+
+    assertEquals(data.passcode, "passX");
+    assertEquals(data.passCode, "pass2");
+  }
 }