aboutsummaryrefslogtreecommitdiff
path: root/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
diff options
context:
space:
mode:
Diffstat (limited to 'gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java')
-rw-r--r--gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java347
1 files changed, 292 insertions, 55 deletions
diff --git a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
index 95d01ace..5ddac50e 100644
--- a/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
+++ b/gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
@@ -19,6 +19,7 @@ package com.google.gson.internal.bind;
import com.google.gson.FieldNamingStrategy;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
+import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.ReflectionAccessFilter.FilterResult;
@@ -38,11 +39,18 @@ import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
+import java.lang.reflect.AccessibleObject;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -88,88 +96,137 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
List<String> fieldNames = new ArrayList<>(alternates.length + 1);
fieldNames.add(serializedName);
- for (String alternate : alternates) {
- fieldNames.add(alternate);
- }
+ Collections.addAll(fieldNames, alternates);
return fieldNames;
}
- @Override public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
+ @Override
+ public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
Class<? super T> raw = type.getRawType();
if (!Object.class.isAssignableFrom(raw)) {
return null; // it's a primitive!
}
- FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
+ FilterResult filterResult =
+ ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
if (filterResult == FilterResult.BLOCK_ALL) {
- throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for "
- + raw + ". Register a TypeAdapter for this type or adjust the access filter.");
+ throw new JsonIOException(
+ "ReflectionAccessFilter does not permit using reflection for " + raw
+ + ". Register a TypeAdapter for this type or adjust the access filter.");
}
boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
+ // If the type is actually a Java Record, we need to use the RecordAdapter instead. This will always be false
+ // on JVMs that do not support records.
+ if (ReflectionHelper.isRecord(raw)) {
+ @SuppressWarnings("unchecked")
+ TypeAdapter<T> adapter = (TypeAdapter<T>) new RecordAdapter<>(raw,
+ getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible);
+ return adapter;
+ }
+
ObjectConstructor<T> constructor = constructorConstructor.get(type);
- return new Adapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible));
+ return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
}
- private static void checkAccessible(Object object, Field field) {
- if (!ReflectionAccessFilterHelper.canAccess(field, Modifier.isStatic(field.getModifiers()) ? null : object)) {
- throw new JsonIOException("Field '" + field.getDeclaringClass().getName() + "#"
- + field.getName() + "' is not accessible and ReflectionAccessFilter does not "
- + "permit making it accessible. Register a TypeAdapter for the declaring type "
- + "or adjust the access filter.");
+ private static <M extends AccessibleObject & Member> void checkAccessible(Object object, M member) {
+ if (!ReflectionAccessFilterHelper.canAccess(member, Modifier.isStatic(member.getModifiers()) ? null : object)) {
+ String memberDescription = ReflectionHelper.getAccessibleObjectDescription(member, true);
+ throw new JsonIOException(memberDescription + " is not accessible and ReflectionAccessFilter does not"
+ + " permit making it accessible. Register a TypeAdapter for the declaring type, adjust the"
+ + " access filter or increase the visibility of the element and its declaring type.");
}
}
private ReflectiveTypeAdapterFactory.BoundField createBoundField(
- final Gson context, final Field field, final String name,
+ final Gson context, final Field field, final Method accessor, final String name,
final TypeToken<?> fieldType, boolean serialize, boolean deserialize,
final boolean blockInaccessible) {
+
final boolean isPrimitive = Primitives.isPrimitive(fieldType.getRawType());
- // special casing primitives here saves ~5% on Android...
+
+ int modifiers = field.getModifiers();
+ final boolean isStaticFinalField = Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers);
+
JsonAdapter annotation = field.getAnnotation(JsonAdapter.class);
TypeAdapter<?> mapped = null;
if (annotation != null) {
+ // This is not safe; requires that user has specified correct adapter class for @JsonAdapter
mapped = jsonAdapterFactory.getTypeAdapter(
constructorConstructor, context, fieldType, annotation);
}
final boolean jsonAdapterPresent = mapped != null;
if (mapped == null) mapped = context.getAdapter(fieldType);
- final TypeAdapter<?> typeAdapter = mapped;
- return new ReflectiveTypeAdapterFactory.BoundField(name, serialize, deserialize) {
- @SuppressWarnings({"unchecked", "rawtypes"}) // the type adapter and field type always agree
- @Override void write(JsonWriter writer, Object value)
+ @SuppressWarnings("unchecked")
+ final TypeAdapter<Object> typeAdapter = (TypeAdapter<Object>) mapped;
+ return new ReflectiveTypeAdapterFactory.BoundField(name, field.getName(), serialize, deserialize) {
+ @Override void write(JsonWriter writer, Object source)
throws IOException, IllegalAccessException {
if (!serialized) return;
if (blockInaccessible) {
- checkAccessible(value, field);
+ if (accessor == null) {
+ checkAccessible(source, field);
+ } else {
+ // Note: This check might actually be redundant because access check for canonical
+ // constructor should have failed already
+ checkAccessible(source, accessor);
+ }
}
- Object fieldValue = field.get(value);
- if (fieldValue == value) {
+ Object fieldValue;
+ if (accessor != null) {
+ try {
+ fieldValue = accessor.invoke(source);
+ } catch (InvocationTargetException e) {
+ String accessorDescription = ReflectionHelper.getAccessibleObjectDescription(accessor, false);
+ throw new JsonIOException("Accessor " + accessorDescription + " threw exception", e.getCause());
+ }
+ } else {
+ fieldValue = field.get(source);
+ }
+ if (fieldValue == source) {
// avoid direct recursion
return;
}
writer.name(name);
- TypeAdapter t = jsonAdapterPresent ? typeAdapter
- : new TypeAdapterRuntimeTypeWrapper(context, typeAdapter, fieldType.getType());
+ TypeAdapter<Object> t = jsonAdapterPresent ? typeAdapter
+ : new TypeAdapterRuntimeTypeWrapper<>(context, typeAdapter, fieldType.getType());
t.write(writer, fieldValue);
}
- @Override void read(JsonReader reader, Object value)
+
+ @Override
+ void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException, JsonParseException {
+ Object fieldValue = typeAdapter.read(reader);
+ if (fieldValue == null && isPrimitive) {
+ throw new JsonParseException("null is not allowed as value for record component '" + fieldName + "'"
+ + " of primitive type; at path " + reader.getPath());
+ }
+ target[index] = fieldValue;
+ }
+
+ @Override
+ void readIntoField(JsonReader reader, Object target)
throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);
if (fieldValue != null || !isPrimitive) {
if (blockInaccessible) {
- checkAccessible(value, field);
+ checkAccessible(target, field);
+ } else if (isStaticFinalField) {
+ // Reflection does not permit setting value of `static final` field, even after calling `setAccessible`
+ // Handle this here to avoid causing IllegalAccessException when calling `Field.set`
+ String fieldDescription = ReflectionHelper.getAccessibleObjectDescription(field, false);
+ throw new JsonIOException("Cannot set value of 'static final' " + fieldDescription);
}
- field.set(value, fieldValue);
+ field.set(target, fieldValue);
}
}
};
}
- private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw, boolean blockInaccessible) {
+ private Map<String, BoundField> getBoundFields(Gson context, TypeToken<?> type, Class<?> raw,
+ boolean blockInaccessible, boolean isRecord) {
Map<String, BoundField> result = new LinkedHashMap<>();
if (raw.isInterface()) {
return result;
@@ -184,9 +241,9 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
if (raw != originalRaw && fields.length > 0) {
FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
if (filterResult == FilterResult.BLOCK_ALL) {
- throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for "
- + raw + " (supertype of " + originalRaw + "). Register a TypeAdapter for this type "
- + "or adjust the access filter.");
+ throw new JsonIOException("ReflectionAccessFilter does not permit using reflection for " + raw
+ + " (supertype of " + originalRaw + "). Register a TypeAdapter for this type"
+ + " or adjust the access filter.");
}
blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;
}
@@ -197,9 +254,36 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
if (!serialize && !deserialize) {
continue;
}
+ // The accessor method is only used for records. If the type is a record, we will read out values
+ // via its accessor method instead of via reflection. This way we will bypass the accessible restrictions
+ Method accessor = null;
+ if (isRecord) {
+ // If there is a static field on a record, there will not be an accessor. Instead we will use the default
+ // field serialization logic, but for deserialization the field is excluded for simplicity. Note that Gson
+ // ignores static fields by default, but GsonBuilder.excludeFieldsWithModifiers can overwrite this.
+ if (Modifier.isStatic(field.getModifiers())) {
+ deserialize = false;
+ } else {
+ accessor = ReflectionHelper.getAccessor(raw, field);
+ // If blockInaccessible, skip and perform access check later
+ if (!blockInaccessible) {
+ ReflectionHelper.makeAccessible(accessor);
+ }
+
+ // @SerializedName can be placed on accessor method, but it is not supported there
+ // If field and method have annotation it is not easily possible to determine if accessor method
+ // is implicit and has inherited annotation, or if it is explicitly declared with custom annotation
+ if (accessor.getAnnotation(SerializedName.class) != null
+ && field.getAnnotation(SerializedName.class) == null) {
+ String methodDescription = ReflectionHelper.getAccessibleObjectDescription(accessor, false);
+ throw new JsonIOException("@SerializedName on " + methodDescription + " is not supported");
+ }
+ }
+ }
// If blockInaccessible, skip and perform access check later
- if (!blockInaccessible) {
+ // For Records if the accessor method is used the field does not have to be made accessible
+ if (!blockInaccessible && accessor == null) {
ReflectionHelper.makeAccessible(field);
}
Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType());
@@ -208,7 +292,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
for (int i = 0, size = fieldNames.size(); i < size; ++i) {
String name = fieldNames.get(i);
if (i != 0) serialize = false; // only serialize the default name
- BoundField boundField = createBoundField(context, field, name,
+ BoundField boundField = createBoundField(context, field, accessor, name,
TypeToken.get(fieldType), serialize, deserialize, blockInaccessible);
BoundField replaced = result.put(name, boundField);
if (previous == null) previous = replaced;
@@ -226,34 +310,75 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
static abstract class BoundField {
final String name;
+ /** Name of the underlying field */
+ final String fieldName;
final boolean serialized;
final boolean deserialized;
- protected BoundField(String name, boolean serialized, boolean deserialized) {
+ protected BoundField(String name, String fieldName, boolean serialized, boolean deserialized) {
this.name = name;
+ this.fieldName = fieldName;
this.serialized = serialized;
this.deserialized = deserialized;
}
- abstract void write(JsonWriter writer, Object value) throws IOException, IllegalAccessException;
- abstract void read(JsonReader reader, Object value) throws IOException, IllegalAccessException;
+
+ /** Read this field value from the source, and append its JSON value to the writer */
+ abstract void write(JsonWriter writer, Object source) throws IOException, IllegalAccessException;
+
+ /** Read the value into the target array, used to provide constructor arguments for records */
+ abstract void readIntoArray(JsonReader reader, int index, Object[] target) throws IOException, JsonParseException;
+
+ /** Read the value from the reader, and set it on the corresponding field on target via reflection */
+ abstract void readIntoField(JsonReader reader, Object target) throws IOException, IllegalAccessException;
}
- public static final class Adapter<T> extends TypeAdapter<T> {
- private final ObjectConstructor<T> constructor;
- private final Map<String, BoundField> boundFields;
+ /**
+ * Base class for Adapters produced by this factory.
+ *
+ * <p>The {@link RecordAdapter} is a special case to handle records for JVMs that support it, for
+ * all other types we use the {@link FieldReflectionAdapter}. This class encapsulates the common
+ * logic for serialization and deserialization. During deserialization, we construct an
+ * accumulator A, which we use to accumulate values from the source JSON. After the object has been read in
+ * full, the {@link #finalize(Object)} method is used to convert the accumulator to an instance
+ * of T.
+ *
+ * @param <T> type of objects that this Adapter creates.
+ * @param <A> type of accumulator used to build the deserialization result.
+ */
+ // This class is public because external projects check for this class with `instanceof` (even though it is internal)
+ public static abstract class Adapter<T, A> extends TypeAdapter<T> {
+ final Map<String, BoundField> boundFields;
- Adapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
- this.constructor = constructor;
+ Adapter(Map<String, BoundField> boundFields) {
this.boundFields = boundFields;
}
- @Override public T read(JsonReader in) throws IOException {
+ @Override
+ public void write(JsonWriter out, T value) throws IOException {
+ if (value == null) {
+ out.nullValue();
+ return;
+ }
+
+ out.beginObject();
+ try {
+ for (BoundField boundField : boundFields.values()) {
+ boundField.write(out, value);
+ }
+ } catch (IllegalAccessException e) {
+ throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
+ }
+ out.endObject();
+ }
+
+ @Override
+ public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
- T instance = constructor.construct();
+ A accumulator = createAccumulator();
try {
in.beginObject();
@@ -263,7 +388,7 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
if (field == null || !field.deserialized) {
in.skipValue();
} else {
- field.read(in, instance);
+ readField(accumulator, in, field);
}
}
} catch (IllegalStateException e) {
@@ -272,24 +397,136 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
in.endObject();
- return instance;
+ return finalize(accumulator);
}
- @Override public void write(JsonWriter out, T value) throws IOException {
- if (value == null) {
- out.nullValue();
- return;
+ /** Create the Object that will be used to collect each field value */
+ abstract A createAccumulator();
+ /**
+ * Read a single BoundField into the accumulator. The JsonReader will be pointed at the
+ * start of the value for the BoundField to read from.
+ */
+ abstract void readField(A accumulator, JsonReader in, BoundField field)
+ throws IllegalAccessException, IOException;
+ /** Convert the accumulator to a final instance of T. */
+ abstract T finalize(A accumulator);
+ }
+
+ private static final class FieldReflectionAdapter<T> extends Adapter<T, T> {
+ private final ObjectConstructor<T> constructor;
+
+ FieldReflectionAdapter(ObjectConstructor<T> constructor, Map<String, BoundField> boundFields) {
+ super(boundFields);
+ this.constructor = constructor;
+ }
+
+ @Override
+ T createAccumulator() {
+ return constructor.construct();
+ }
+
+ @Override
+ void readField(T accumulator, JsonReader in, BoundField field)
+ throws IllegalAccessException, IOException {
+ field.readIntoField(in, accumulator);
+ }
+
+ @Override
+ T finalize(T accumulator) {
+ return accumulator;
+ }
+ }
+
+ private static final class RecordAdapter<T> extends Adapter<T, Object[]> {
+ static final Map<Class<?>, Object> PRIMITIVE_DEFAULTS = primitiveDefaults();
+
+ // The canonical constructor of the record
+ private final Constructor<T> constructor;
+ // Array of arguments to the constructor, initialized with default values for primitives
+ private final Object[] constructorArgsDefaults;
+ // Map from component names to index into the constructors arguments.
+ private final Map<String, Integer> componentIndices = new HashMap<>();
+
+ RecordAdapter(Class<T> raw, Map<String, BoundField> boundFields, boolean blockInaccessible) {
+ super(boundFields);
+ constructor = ReflectionHelper.getCanonicalRecordConstructor(raw);
+
+ if (blockInaccessible) {
+ checkAccessible(null, constructor);
+ } else {
+ // Ensure the constructor is accessible
+ ReflectionHelper.makeAccessible(constructor);
}
- out.beginObject();
+ String[] componentNames = ReflectionHelper.getRecordComponentNames(raw);
+ for (int i = 0; i < componentNames.length; i++) {
+ componentIndices.put(componentNames[i], i);
+ }
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+
+ // We need to ensure that we are passing non-null values to primitive fields in the constructor. To do this,
+ // we create an Object[] where all primitives are initialized to non-null values.
+ constructorArgsDefaults = new Object[parameterTypes.length];
+ for (int i = 0; i < parameterTypes.length; i++) {
+ // This will correctly be null for non-primitive types:
+ constructorArgsDefaults[i] = PRIMITIVE_DEFAULTS.get(parameterTypes[i]);
+ }
+ }
+
+ private static Map<Class<?>, Object> primitiveDefaults() {
+ Map<Class<?>, Object> zeroes = new HashMap<>();
+ zeroes.put(byte.class, (byte) 0);
+ zeroes.put(short.class, (short) 0);
+ zeroes.put(int.class, 0);
+ zeroes.put(long.class, 0L);
+ zeroes.put(float.class, 0F);
+ zeroes.put(double.class, 0D);
+ zeroes.put(char.class, '\0');
+ zeroes.put(boolean.class, false);
+ return zeroes;
+ }
+
+ @Override
+ Object[] createAccumulator() {
+ return constructorArgsDefaults.clone();
+ }
+
+ @Override
+ void readField(Object[] accumulator, JsonReader in, BoundField field) throws IOException {
+ // Obtain the component index from the name of the field backing it
+ Integer componentIndex = componentIndices.get(field.fieldName);
+ if (componentIndex == null) {
+ throw new IllegalStateException(
+ "Could not find the index in the constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
+ + " for field with name '" + field.fieldName + "',"
+ + " unable to determine which argument in the constructor the field corresponds"
+ + " to. This is unexpected behavior, as we expect the RecordComponents to have the"
+ + " same names as the fields in the Java class, and that the order of the"
+ + " RecordComponents is the same as the order of the canonical constructor parameters.");
+ }
+ field.readIntoArray(in, componentIndex, accumulator);
+ }
+
+ @Override
+ T finalize(Object[] accumulator) {
try {
- for (BoundField boundField : boundFields.values()) {
- boundField.write(out, value);
- }
+ return constructor.newInstance(accumulator);
} catch (IllegalAccessException e) {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
- out.endObject();
+ // Note: InstantiationException should be impossible because record class is not abstract;
+ // IllegalArgumentException should not be possible unless a bad adapter returns objects of the wrong type
+ catch (InstantiationException | IllegalArgumentException e) {
+ throw new RuntimeException(
+ "Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
+ + " with args " + Arrays.toString(accumulator), e);
+ }
+ catch (InvocationTargetException e) {
+ // TODO: JsonParseException ?
+ throw new RuntimeException(
+ "Failed to invoke constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
+ + " with args " + Arrays.toString(accumulator), e.getCause());
+ }
}
}
}