Description
PR #2151 introduced an override for GenericData.containsKey(Object name) that queries classInfo.hasFieldInfo(fieldName). However, ClassInfo.hasFieldInfo checks only whether the @Key field is declared in class reflection metadata, rather than checking whether the field holds a non-null value on the instance.
Impact & Broken Invariants
In GenericData, declared fields with null values are treated as absent from the map:
keySet() / entrySet() Contradiction:
On new MyData(), containsKey("field") returns true, but get("field") is null, entrySet() has size 0, and keySet().toString() outputs [].
Set.contains vs Iterator Inconsistency:
Because java.util.AbstractMap.keySet().contains(k) delegates to Map.containsKey(k), model.keySet().contains("field") evaluates to true, while iterating over model.keySet() yields 0 elements.
- Client Breakages:
Code patterns checking for field presence (such as pagination checks like if (response.containsKey("pageToken"))) now evaluate to true even when the server never populated the field.
Reproduction
public class ExampleModel extends GenericData {
@Key private String optionalField;
}
ExampleModel model = new ExampleModel();
// Prior to 2.2.0:
// model.containsKey("optionalField") == false
// In 2.2.0:
model.containsKey("optionalField"); // returns true!
model.get("optionalField"); // returns null
model.keySet(); // prints []
model.keySet().contains("optionalField"); // returns true while iterator is empty
Proposed Fix
In com.google.api.client.util.GenericData.java, check whether the declared field value is non-null, matching DataMap.containsKey():
@Override
public final boolean containsKey(Object name) {
if (!(name instanceof String)) {
return false;
}
String fieldName = (String) name;
FieldInfo fieldInfo = classInfo.getFieldInfo(fieldName);
if (fieldInfo != null) {
return fieldInfo.getValue(this) != null;
}
if (classInfo.getIgnoreCase()) {
fieldName = fieldName.toLowerCase(Locale.US);
}
return unknownFields.containsKey(fieldName);
}
Description
PR #2151 introduced an override for
GenericData.containsKey(Object name)that queriesclassInfo.hasFieldInfo(fieldName). However,ClassInfo.hasFieldInfochecks only whether the@Keyfield is declared in class reflection metadata, rather than checking whether the field holds a non-null value on the instance.Impact & Broken Invariants
In
GenericData, declared fields withnullvalues are treated as absent from the map:keySet()/entrySet()Contradiction:On
new MyData(),containsKey("field")returnstrue, butget("field")isnull,entrySet()has size0, andkeySet().toString()outputs[].Set.containsvsIteratorInconsistency:Because
java.util.AbstractMap.keySet().contains(k)delegates toMap.containsKey(k),model.keySet().contains("field")evaluates totrue, while iterating overmodel.keySet()yields0elements.Code patterns checking for field presence (such as pagination checks like
if (response.containsKey("pageToken"))) now evaluate totrueeven when the server never populated the field.Reproduction
Proposed Fix
In
com.google.api.client.util.GenericData.java, check whether the declared field value is non-null, matchingDataMap.containsKey():