Skip to content

Commit c5a1568

Browse files
Marcono1234tibor-universe
authored andcommitted
Only create one BoundField instance per field in ReflectiveTypeAdapterFactory (google#2440)
* Only create one BoundField instance per field in ReflectiveTypeAdapterFactory Instead of creating a BoundField for every possible name of a field (for SerializedName usage) and then storing for that BoundField whether it is serialized or deserialized, instead only create one BoundField and then have a separate Map<String, BoundField> for deserialized fields, and a separate List<BoundField> for serialized fields. * Fix indentation
1 parent 385275b commit c5a1568

File tree

2 files changed

+94
-43
lines changed

2 files changed

+94
-43
lines changed

gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,8 @@ private static <M extends AccessibleObject & Member> void checkAccessible(Object
130130
}
131131

132132
private BoundField createBoundField(
133-
final Gson context, final Field field, final String name,
134-
final TypeToken<?> fieldType, boolean serialize, boolean deserialize,
135-
final boolean blockInaccessible) {
133+
final Gson context, final Field field, final String serializedName,
134+
final TypeToken<?> fieldType, final boolean serialize, final boolean blockInaccessible) {
136135

137136
final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());
138137

@@ -159,10 +158,9 @@ private BoundField createBoundField(
159158
// Will never actually be used, but we set it to avoid confusing nullness-analysis tools
160159
writeTypeAdapter = typeAdapter;
161160
}
162-
return new BoundField(name, field, serialize, deserialize) {
161+
return new BoundField(serializedName, field) {
163162
@Override void write(JsonWriter writer, Object source)
164163
throws IOException, IllegalAccessException {
165-
if (!serialized) return;
166164
if (blockInaccessible) {
167165
checkAccessible(source, field);
168166
}
@@ -173,7 +171,7 @@ private BoundField createBoundField(
173171
// avoid direct recursion
174172
return;
175173
}
176-
writer.name(name);
174+
writer.name(serializedName);
177175
writeTypeAdapter.write(writer, fieldValue);
178176
}
179177

@@ -196,13 +194,35 @@ void readIntoField(JsonReader reader, Object target)
196194
};
197195
}
198196

199-
private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw,
200-
boolean blockInaccessible) {
201-
Map<String, BoundField> result = new LinkedHashMap<>();
197+
private static class FieldsData {
198+
public static final FieldsData EMPTY = new FieldsData(Collections.<String, BoundField>emptyMap(), Collections.<BoundField>emptyList());
199+
200+
/** Maps from JSON member name to field */
201+
public final Map<String, BoundField> deserializedFields;
202+
public final List<BoundField> serializedFields;
203+
204+
public FieldsData(Map<String, BoundField> deserializedFields, List<BoundField> serializedFields) {
205+
this.deserializedFields = deserializedFields;
206+
this.serializedFields = serializedFields;
207+
}
208+
}
209+
210+
private static IllegalArgumentException createDuplicateFieldException(Class<?> declaringType, String duplicateName, Field field1, Field field2) {
211+
throw new IllegalArgumentException("Class " + declaringType.getName()
212+
+ " declares multiple JSON fields named '" + duplicateName + "'; conflict is caused"
213+
+ " by fields " + ReflectionHelper.fieldToString(field1) + " and " + ReflectionHelper.fieldToString(field2)
214+
+ "\nSee " + TroubleshootingGuide.createUrl("duplicate-fields"));
215+
}
216+
217+
private FieldsData getBoundFields(Gson context, TypeToken<?> type, Class<?> raw, boolean blockInaccessible) {
202218
if (raw.isInterface()) {
203-
return result;
219+
return FieldsData.EMPTY;
204220
}
205221

222+
Map<String, BoundField> deserializedFields = new LinkedHashMap<>();
223+
// For serialized fields use a Map to track duplicate field names; otherwise this could be a List<BoundField> instead
224+
Map<String, BoundField> serializedFields = new LinkedHashMap<>();
225+
206226
Class<?> originalRaw = raw;
207227
while (raw != Object.class) {
208228
Field[] fields = raw.getDeclaredFields();
@@ -229,44 +249,47 @@ private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type,
229249
if (!blockInaccessible) {
230250
ReflectionHelper.makeAccessible(field);
231251
}
252+
232253
Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType());
233254
List<String> fieldNames = getFieldNames(field);
234-
BoundField previous = null;
235-
for (int i = 0, size = fieldNames.size(); i < size; ++i) {
236-
String name = fieldNames.get(i);
237-
if (i != 0) serialize = false; // only serialize the default name
238-
BoundField boundField = createBoundField(context, field, name,
239-
TypeToken.get(fieldType), serialize, deserialize, blockInaccessible);
240-
BoundField replaced = result.put(name, boundField);
241-
if (previous == null) previous = replaced;
255+
String serializedName = fieldNames.get(0);
256+
BoundField boundField = createBoundField(context, field, serializedName,
257+
TypeToken.get(fieldType), serialize, blockInaccessible);
258+
259+
if (deserialize) {
260+
for (String name : fieldNames) {
261+
BoundField replaced = deserializedFields.put(name, boundField);
262+
263+
if (replaced != null) {
264+
throw createDuplicateFieldException(originalRaw, name, replaced.field, field);
265+
}
266+
}
242267
}
243-
if (previous != null) {
244-
throw new IllegalArgumentException("Class " + originalRaw.getName()
245-
+ " declares multiple JSON fields named '" + previous.name + "'; conflict is caused"
246-
+ " by fields " + ReflectionHelper.fieldToString(previous.field) + " and " + ReflectionHelper.fieldToString(field)
247-
+ "\nSee " + TroubleshootingGuide.createUrl("duplicate-fields"));
268+
269+
if (serialize) {
270+
BoundField replaced = serializedFields.put(serializedName, boundField);
271+
if (replaced != null) {
272+
throw createDuplicateFieldException(originalRaw, serializedName, replaced.field, field);
273+
}
248274
}
249275
}
250276
type = TypeToken.get($Gson$Types.resolve(type.getType(), raw, raw.getGenericSuperclass()));
251277
raw = type.getRawType();
252278
}
253-
return result;
279+
return new FieldsData(deserializedFields, new ArrayList<>(serializedFields.values()));
254280
}
255281

256282
static abstract class BoundField {
257-
final String name;
283+
/** Name used for serialization (but not for deserialization) */
284+
final String serializedName;
258285
final Field field;
259286
/** Name of the underlying field */
260287
final String fieldName;
261-
final boolean serialized;
262-
final boolean deserialized;
263288

264-
protected BoundField(String name, Field field, boolean serialized, boolean deserialized) {
265-
this.name = name;
289+
protected BoundField(String serializedName, Field field) {
290+
this.serializedName = serializedName;
266291
this.field = field;
267292
this.fieldName = field.getName();
268-
this.serialized = serialized;
269-
this.deserialized = deserialized;
270293
}
271294

272295
/** Read this field value from the source, and append its JSON value to the writer */
@@ -288,10 +311,10 @@ protected BoundField(String name, Field field, boolean serialized, boolean deser
288311
*/
289312
// This class is public because external projects check for this class with `instanceof` (even though it is internal)
290313
public static abstract class Adapter<T, A> extends TypeAdapter<T> {
291-
final Map<String, BoundField> boundFields;
314+
private final FieldsData fieldsData;
292315

293-
Adapter(Map<String, BoundField> boundFields) {
294-
this.boundFields = boundFields;
316+
Adapter(FieldsData fieldsData) {
317+
this.fieldsData = fieldsData;
295318
}
296319

297320
@Override
@@ -303,7 +326,7 @@ public void write(JsonWriter out, T value) throws IOException {
303326

304327
out.beginObject();
305328
try {
306-
for (BoundField boundField : boundFields.values()) {
329+
for (BoundField boundField : fieldsData.serializedFields) {
307330
boundField.write(out, value);
308331
}
309332
} catch (IllegalAccessException e) {
@@ -320,13 +343,14 @@ public T read(JsonReader in) throws IOException {
320343
}
321344

322345
A accumulator = createAccumulator();
346+
Map<String, BoundField> deserializedFields = fieldsData.deserializedFields;
323347

324348
try {
325349
in.beginObject();
326350
while (in.hasNext()) {
327351
String name = in.nextName();
328-
BoundField field = boundFields.get(name);
329-
if (field == null || !field.deserialized) {
352+
BoundField field = deserializedFields.get(name);
353+
if (field == null) {
330354
in.skipValue();
331355
} else {
332356
readField(accumulator, in, field);
@@ -356,8 +380,8 @@ abstract void readField(A accumulator, JsonReader in, BoundField field)
356380
private static final class FieldReflectionAdapter<T> extends Adapter<T, T> {
357381
private final ObjectConstructor<T> constructor;
358382

359-
FieldReflectionAdapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
360-
super(boundFields);
383+
FieldReflectionAdapter(ObjectConstructor<T> constructor, FieldsData fieldsData) {
384+
super(fieldsData);
361385
this.constructor = constructor;
362386
}
363387

gson/src/test/java/com/google/gson/functional/ObjectTest.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import static com.google.common.truth.Truth.assertThat;
2020
import static org.junit.Assert.fail;
2121

22+
import com.google.gson.ExclusionStrategy;
23+
import com.google.gson.FieldAttributes;
2224
import com.google.gson.Gson;
2325
import com.google.gson.GsonBuilder;
2426
import com.google.gson.InstanceCreator;
@@ -171,14 +173,39 @@ private static class Superclass2 {
171173

172174
@Test
173175
public void testClassWithDuplicateFields() {
176+
String expectedMessage = "Class com.google.gson.functional.ObjectTest$Subclass declares multiple JSON fields named 's';"
177+
+ " conflict is caused by fields com.google.gson.functional.ObjectTest$Superclass1#s and"
178+
+ " com.google.gson.functional.ObjectTest$Superclass2#s"
179+
+ "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#duplicate-fields";
180+
181+
try {
182+
gson.getAdapter(Subclass.class);
183+
fail();
184+
} catch (IllegalArgumentException e) {
185+
assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
186+
}
187+
188+
// Detection should also work properly when duplicate fields exist only for serialization
189+
Gson gson = new GsonBuilder()
190+
.addDeserializationExclusionStrategy(new ExclusionStrategy() {
191+
@Override
192+
public boolean shouldSkipField(FieldAttributes f) {
193+
// Skip all fields for deserialization
194+
return true;
195+
}
196+
197+
@Override
198+
public boolean shouldSkipClass(Class<?> clazz) {
199+
return false;
200+
}
201+
})
202+
.create();
203+
174204
try {
175205
gson.getAdapter(Subclass.class);
176206
fail();
177207
} catch (IllegalArgumentException e) {
178-
assertThat(e).hasMessageThat().isEqualTo("Class com.google.gson.functional.ObjectTest$Subclass declares multiple JSON fields named 's';"
179-
+ " conflict is caused by fields com.google.gson.functional.ObjectTest$Superclass1#s and"
180-
+ " com.google.gson.functional.ObjectTest$Superclass2#s"
181-
+ "\nSee https://github.com/google/gson/blob/main/Troubleshooting.md#duplicate-fields");
208+
assertThat(e).hasMessageThat().isEqualTo(expectedMessage);
182209
}
183210
}
184211

0 commit comments

Comments
 (0)