diff --git a/gson/src/main/java/com/google/gson/Gson.java b/gson/src/main/java/com/google/gson/Gson.java
index 45336a87e9..d6814fc5ea 100644
--- a/gson/src/main/java/com/google/gson/Gson.java
+++ b/gson/src/main/java/com/google/gson/Gson.java
@@ -151,6 +151,8 @@ public final class Gson {
static final FieldNamingStrategy DEFAULT_FIELD_NAMING_STRATEGY = FieldNamingPolicy.IDENTITY;
static final ToNumberStrategy DEFAULT_OBJECT_TO_NUMBER_STRATEGY = ToNumberPolicy.DOUBLE;
static final ToNumberStrategy DEFAULT_NUMBER_TO_NUMBER_STRATEGY = ToNumberPolicy.LAZILY_PARSED_NUMBER;
+ static final MissingFieldValueStrategy DEFAULT_MISSING_FIELD_VALUE_STRATEGY = MissingFieldValueStrategy.DO_NOTHING;
+ static final UnknownFieldStrategy DEFAULT_UNKNOWN_FIELD_STRATEGY = UnknownFieldStrategy.IGNORE;
private static final String JSON_NON_EXECUTABLE_PREFIX = ")]}'\n";
@@ -195,6 +197,8 @@ public final class Gson {
final List builderHierarchyFactories;
final ToNumberStrategy objectToNumberStrategy;
final ToNumberStrategy numberToNumberStrategy;
+ final MissingFieldValueStrategy missingFieldValueStrategy;
+ final UnknownFieldStrategy unknownFieldStrategy;
final List reflectionFilters;
/**
@@ -242,6 +246,7 @@ public Gson() {
LongSerializationPolicy.DEFAULT, DEFAULT_DATE_PATTERN, DateFormat.DEFAULT, DateFormat.DEFAULT,
Collections.emptyList(), Collections.emptyList(),
Collections.emptyList(), DEFAULT_OBJECT_TO_NUMBER_STRATEGY, DEFAULT_NUMBER_TO_NUMBER_STRATEGY,
+ DEFAULT_MISSING_FIELD_VALUE_STRATEGY, DEFAULT_UNKNOWN_FIELD_STRATEGY,
Collections.emptyList());
}
@@ -255,6 +260,7 @@ public Gson() {
List builderHierarchyFactories,
List factoriesToBeAdded,
ToNumberStrategy objectToNumberStrategy, ToNumberStrategy numberToNumberStrategy,
+ MissingFieldValueStrategy missingFieldValueStrategy, UnknownFieldStrategy unknownFieldStrategy,
List reflectionFilters) {
this.excluder = excluder;
this.fieldNamingStrategy = fieldNamingStrategy;
@@ -276,6 +282,8 @@ public Gson() {
this.builderHierarchyFactories = builderHierarchyFactories;
this.objectToNumberStrategy = objectToNumberStrategy;
this.numberToNumberStrategy = numberToNumberStrategy;
+ this.missingFieldValueStrategy = missingFieldValueStrategy;
+ this.unknownFieldStrategy = unknownFieldStrategy;
this.reflectionFilters = reflectionFilters;
List factories = new ArrayList<>();
@@ -341,7 +349,8 @@ public Gson() {
factories.add(jsonAdapterFactory);
factories.add(TypeAdapters.ENUM_FACTORY);
factories.add(new ReflectiveTypeAdapterFactory(
- constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory, reflectionFilters));
+ constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory,
+ missingFieldValueStrategy, unknownFieldStrategy, reflectionFilters));
this.factories = Collections.unmodifiableList(factories);
}
diff --git a/gson/src/main/java/com/google/gson/GsonBuilder.java b/gson/src/main/java/com/google/gson/GsonBuilder.java
index f8f1b27f80..b9267eaef8 100644
--- a/gson/src/main/java/com/google/gson/GsonBuilder.java
+++ b/gson/src/main/java/com/google/gson/GsonBuilder.java
@@ -22,10 +22,12 @@
import static com.google.gson.Gson.DEFAULT_FORMATTING_STYLE;
import static com.google.gson.Gson.DEFAULT_JSON_NON_EXECUTABLE;
import static com.google.gson.Gson.DEFAULT_LENIENT;
+import static com.google.gson.Gson.DEFAULT_MISSING_FIELD_VALUE_STRATEGY;
import static com.google.gson.Gson.DEFAULT_NUMBER_TO_NUMBER_STRATEGY;
import static com.google.gson.Gson.DEFAULT_OBJECT_TO_NUMBER_STRATEGY;
import static com.google.gson.Gson.DEFAULT_SERIALIZE_NULLS;
import static com.google.gson.Gson.DEFAULT_SPECIALIZE_FLOAT_VALUES;
+import static com.google.gson.Gson.DEFAULT_UNKNOWN_FIELD_STRATEGY;
import static com.google.gson.Gson.DEFAULT_USE_JDK_UNSAFE;
import com.google.gson.annotations.Since;
@@ -56,7 +58,7 @@
* use {@code new Gson()}. {@code GsonBuilder} is best used by creating it, and then invoking its
* various configuration methods, and finally calling create.
*
- * The following is an example shows how to use the {@code GsonBuilder} to construct a Gson
+ *
The following example shows how to use the {@code GsonBuilder} to construct a Gson
* instance:
*
*
reflectionFilters = new ArrayDeque<>();
/**
@@ -141,6 +145,8 @@ public GsonBuilder() {
this.useJdkUnsafe = gson.useJdkUnsafe;
this.objectToNumberStrategy = gson.objectToNumberStrategy;
this.numberToNumberStrategy = gson.numberToNumberStrategy;
+ this.missingFieldValueStrategy = gson.missingFieldValueStrategy;
+ this.unknownFieldStrategy = gson.unknownFieldStrategy;
this.reflectionFilters.addAll(gson.reflectionFilters);
}
@@ -388,6 +394,37 @@ public GsonBuilder setObjectToNumberStrategy(ToNumberStrategy objectToNumberStra
return this;
}
+ /**
+ * Configures Gson to apply a specific missing field value strategy during deserialization.
+ * The strategy is used during reflection-based deserialization when the JSON data does
+ * not contain a value for a field. A field with explicit JSON null is not considered missing.
+ *
+ * @param missingFieldValueStrategy strategy handling missing field values
+ * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+ * @see MissingFieldValueStrategy#DO_NOTHING The default missing field value strategy
+ * @since $next-version$
+ */
+ public GsonBuilder setMissingFieldValueStrategy(MissingFieldValueStrategy missingFieldValueStrategy) {
+ this.missingFieldValueStrategy = Objects.requireNonNull(missingFieldValueStrategy);
+ return this;
+ }
+
+ /**
+ * Configures Gson to apply a specific unknown field strategy during deserialization.
+ * The strategy is used during reflection-based deserialization when an unknown field
+ * is encountered in the JSON data. If a field which is excluded from deserialization
+ * appears in the JSON data it is considered unknown as well.
+ *
+ * @param unknownFieldStrategy strategy handling unknown fields
+ * @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
+ * @see UnknownFieldStrategy#IGNORE The default unknown field strategy
+ * @since $next-version$
+ */
+ public GsonBuilder setUnknownFieldStrategy(UnknownFieldStrategy unknownFieldStrategy) {
+ this.unknownFieldStrategy = Objects.requireNonNull(unknownFieldStrategy);
+ return this;
+ }
+
/**
* Configures Gson to apply a specific number strategy during deserialization of {@link Number}.
*
@@ -782,7 +819,8 @@ public Gson create() {
serializeSpecialFloatingPointValues, useJdkUnsafe, longSerializationPolicy,
datePattern, dateStyle, timeStyle, new ArrayList<>(this.factories),
new ArrayList<>(this.hierarchyFactories), factories,
- objectToNumberStrategy, numberToNumberStrategy, new ArrayList<>(reflectionFilters));
+ objectToNumberStrategy, numberToNumberStrategy,
+ missingFieldValueStrategy, unknownFieldStrategy, new ArrayList<>(reflectionFilters));
}
private void addTypeAdaptersForDate(String datePattern, int dateStyle, int timeStyle,
diff --git a/gson/src/main/java/com/google/gson/MissingFieldValueStrategy.java b/gson/src/main/java/com/google/gson/MissingFieldValueStrategy.java
new file mode 100644
index 0000000000..8845b2b5a1
--- /dev/null
+++ b/gson/src/main/java/com/google/gson/MissingFieldValueStrategy.java
@@ -0,0 +1,77 @@
+package com.google.gson;
+
+import com.google.gson.internal.reflect.ReflectionHelper;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Field;
+
+/**
+ * A strategy defining how to handle missing field values during reflection-based deserialization.
+ *
+ * @see GsonBuilder#setMissingFieldValueStrategy(MissingFieldValueStrategy)
+ * @since $next-version$
+ */
+public interface MissingFieldValueStrategy {
+ /**
+ * This strategy does nothing when a missing field is detected, it preserves the initial field
+ * value, if any.
+ *
+ * This is the default missing field value strategy.
+ */
+ MissingFieldValueStrategy DO_NOTHING = new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field, TypeToken> resolvedFieldType) {
+ // Preserve initial field value
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "MissingFieldValueStrategy.DO_NOTHING";
+ }
+ };
+
+ /**
+ * This strategy throws an exception when a missing field is detected.
+ */
+ MissingFieldValueStrategy THROW_EXCEPTION = new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field, TypeToken> resolvedFieldType) {
+ // TODO: Proper exception
+ throw new RuntimeException("Missing value for field '" + ReflectionHelper.fieldToString(field) + "'");
+ }
+
+ @Override
+ public String toString() {
+ return "MissingFieldValueStrategy.THROW_EXCEPTION";
+ }
+ };
+
+ /**
+ * Called when a missing field value is detected. Implementations can either throw an exception or
+ * return a default value.
+ *
+ *
Returning {@code null} will keep the initial field value, if any. For example when returning
+ * {@code null} for the field {@code String f = "default"}, the field will still have the value
+ * {@code "default"} afterwards (assuming the constructor of the class was called, see also
+ * {@link GsonBuilder#disableJdkUnsafe()}). The type of the returned value has to match the
+ * type of the field, no narrowing or widening numeric conversion is performed.
+ *
+ *
The {@code instance} represents an instance of the declaring type with the so far already
+ * deserialized fields. It is intended to be used for looking up existing field values to derive
+ * the missing field value from them. Manipulating {@code instance} in any way is not recommended.
+ * For Record classes (Java 16 feature) the {@code instance} is {@code null}.
+ *
+ *
{@code resolvedFieldType} is the type of the field with type variables being resolved, if
+ * possible. For example if {@code class MyClass} has a field {@code T myField} and
+ * {@code MyClass} is deserialized, then {@code resolvedFieldType} will be {@code String}.
+ *
+ * @param declaringType type declaring the field
+ * @param instance instance of the declaring type, {@code null} for Record classes
+ * @param field field whose value is missing
+ * @param resolvedFieldType resolved type of the field
+ * @return the field value, or {@code null}
+ */
+ // TODO: Should this really expose `instance`? Only use case would be to derive value from other fields
+ // but besides that user should not directly manipulate `instance` but return new value instead
+ Object handleMissingField(TypeToken> declaringType, Object instance, Field field, TypeToken> resolvedFieldType);
+}
diff --git a/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java b/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java
index 7736ec7aa0..7c1733b309 100644
--- a/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java
+++ b/gson/src/main/java/com/google/gson/ReflectionAccessFilter.java
@@ -124,6 +124,10 @@ enum FilterResult {
? FilterResult.BLOCK_INACCESSIBLE
: FilterResult.INDECISIVE;
}
+
+ @Override public String toString() {
+ return "ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA";
+ }
};
/**
@@ -149,6 +153,10 @@ enum FilterResult {
? FilterResult.BLOCK_ALL
: FilterResult.INDECISIVE;
}
+
+ @Override public String toString() {
+ return "ReflectionAccessFilter.BLOCK_ALL_JAVA";
+ }
};
/**
@@ -173,6 +181,10 @@ enum FilterResult {
? FilterResult.BLOCK_ALL
: FilterResult.INDECISIVE;
}
+
+ @Override public String toString() {
+ return "ReflectionAccessFilter.BLOCK_ALL_ANDROID";
+ }
};
/**
@@ -198,6 +210,10 @@ enum FilterResult {
? FilterResult.BLOCK_ALL
: FilterResult.INDECISIVE;
}
+
+ @Override public String toString() {
+ return "ReflectionAccessFilter.BLOCK_ALL_PLATFORM";
+ }
};
/**
diff --git a/gson/src/main/java/com/google/gson/UnknownFieldStrategy.java b/gson/src/main/java/com/google/gson/UnknownFieldStrategy.java
new file mode 100644
index 0000000000..7ec4e5206f
--- /dev/null
+++ b/gson/src/main/java/com/google/gson/UnknownFieldStrategy.java
@@ -0,0 +1,79 @@
+package com.google.gson;
+
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import java.io.IOException;
+
+/**
+ * A strategy defining how to handle unknown fields during reflection-based deserialization.
+ *
+ * @see GsonBuilder#setUnknownFieldStrategy(UnknownFieldStrategy)
+ * @since $next-version$
+ */
+public interface UnknownFieldStrategy {
+ /**
+ * This strategy ignores the unknown field.
+ *
+ * This is the default unknown field strategy.
+ */
+ UnknownFieldStrategy IGNORE = new UnknownFieldStrategy() {
+ @Override
+ public void handleUnknownField(TypeToken> declaringType, Object instance, String fieldName,
+ JsonReader jsonReader, Gson gson) throws IOException {
+ jsonReader.skipValue();
+ }
+
+ @Override
+ public String toString() {
+ return "UnknownFieldStrategy.IGNORE";
+ }
+ };
+
+ /**
+ * This strategy throws an exception when an unknown field is encountered.
+ *
+ *
Note: Be careful when using this strategy; while it might sound tempting
+ * to strictly validate that the JSON data matches the expected format, this strategy
+ * makes it difficult to add new fields to the JSON structure in a backward compatible way.
+ * Usually it suffices to use only {@link MissingFieldValueStrategy#THROW_EXCEPTION} for
+ * validation and to ignore unknown fields.
+ */
+ UnknownFieldStrategy THROW_EXCEPTION = new UnknownFieldStrategy() {
+ @Override
+ public void handleUnknownField(TypeToken> declaringType, Object instance, String fieldName,
+ JsonReader jsonReader, Gson gson) throws IOException {
+ // TODO: Proper exception
+ throw new RuntimeException("Unknown field '" + fieldName + "' for " + declaringType.getRawType() + " at path " + jsonReader.getPath());
+ }
+
+ @Override
+ public String toString() {
+ return "UnknownFieldStrategy.THROW_EXCEPTION";
+ }
+ };
+
+ /**
+ * Called when an unknown field is encountered. Implementations can throw an exception,
+ * store the field value in {@code instance} or ignore the unknown field.
+ *
+ *
The {@code jsonReader} is positioned to read the value of the unknown field. If an
+ * implementation of this method does not throw an exception it must consume the value, either
+ * by reading it with methods like {@link JsonReader#nextString()} (possibly after peeking
+ * at the value type first), or by skipping it with {@link JsonReader#skipValue()}.
+ * The {@code gson} object can be used to read from the {@code jsonReader}. It is the same
+ * instance which was originally used to perform the deserialization.
+ *
+ *
The {@code instance} represents an instance of the declaring type with the so far already
+ * deserialized fields. It can be used to store the value of the unknown field, for example
+ * if it declares a {@code transient Map} field for all unknown values.
+ * For Record classes (Java 16 feature) the {@code instance} is {@code null}.
+ *
+ * @param declaringType type declaring the field
+ * @param instance instance of the declaring type, {@code null} for Record classes
+ * @param fieldName name of the unknown field
+ * @param jsonReader reader to be used to read or skip the field value
+ * @param gson {@code Gson} instance which can be used to read the field value from {@code jsonReader}
+ * @throws IOException if reading or skipping the field value fails
+ */
+ void handleUnknownField(TypeToken> declaringType, Object instance, String fieldName, JsonReader jsonReader, Gson gson) throws IOException;
+}
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 9194fc33bf..985d841e4b 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
@@ -21,10 +21,12 @@
import com.google.gson.JsonIOException;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
+import com.google.gson.MissingFieldValueStrategy;
import com.google.gson.ReflectionAccessFilter;
import com.google.gson.ReflectionAccessFilter.FilterResult;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
+import com.google.gson.UnknownFieldStrategy;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import com.google.gson.internal.$Gson$Types;
@@ -63,16 +65,21 @@ public final class ReflectiveTypeAdapterFactory implements TypeAdapterFactory {
private final FieldNamingStrategy fieldNamingPolicy;
private final Excluder excluder;
private final JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory;
+ private final MissingFieldValueStrategy missingFieldValueStrategy;
+ private final UnknownFieldStrategy unknownFieldStrategy;
private final List reflectionFilters;
public ReflectiveTypeAdapterFactory(ConstructorConstructor constructorConstructor,
FieldNamingStrategy fieldNamingPolicy, Excluder excluder,
JsonAdapterAnnotationTypeAdapterFactory jsonAdapterFactory,
+ MissingFieldValueStrategy missingFieldValueStrategy, UnknownFieldStrategy unknownFieldStrategy,
List reflectionFilters) {
this.constructorConstructor = constructorConstructor;
this.fieldNamingPolicy = fieldNamingPolicy;
this.excluder = excluder;
this.jsonAdapterFactory = jsonAdapterFactory;
+ this.missingFieldValueStrategy = missingFieldValueStrategy;
+ this.unknownFieldStrategy = unknownFieldStrategy;
this.reflectionFilters = reflectionFilters;
}
@@ -122,13 +129,15 @@ public TypeAdapter create(Gson gson, final TypeToken type) {
// on JVMs that do not support records.
if (ReflectionHelper.isRecord(raw)) {
@SuppressWarnings("unchecked")
- TypeAdapter adapter = (TypeAdapter) new RecordAdapter<>(raw,
+ TypeAdapter adapter = new RecordAdapter<>(gson,
+ missingFieldValueStrategy, unknownFieldStrategy, type, (Class) raw,
getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible);
return adapter;
}
ObjectConstructor constructor = constructorConstructor.get(type);
- return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
+ return new FieldReflectionAdapter<>(gson, missingFieldValueStrategy, unknownFieldStrategy,
+ constructor, type, getBoundFields(gson, type, raw, blockInaccessible, false));
}
private static void checkAccessible(Object object, M member) {
@@ -170,7 +179,7 @@ private BoundField createBoundField(
// Will never actually be used, but we set it to avoid confusing nullness-analysis tools
writeTypeAdapter = typeAdapter;
}
- return new BoundField(name, field, serialize, deserialize) {
+ return new BoundField(name, field, fieldType, serialize, deserialize) {
@Override void write(JsonWriter writer, Object source)
throws IOException, IllegalAccessException {
if (!serialized) return;
@@ -217,6 +226,11 @@ void readIntoArray(JsonReader reader, int index, Object[] target) throws IOExcep
void readIntoField(JsonReader reader, Object target)
throws IOException, IllegalAccessException {
Object fieldValue = typeAdapter.read(reader);
+ putIntoField(fieldValue, target);
+ }
+
+ @Override
+ void putIntoField(Object fieldValue, Object target) throws IllegalAccessException {
if (fieldValue != null || !isPrimitive) {
if (blockInaccessible) {
checkAccessible(target, field);
@@ -320,25 +334,30 @@ static abstract class BoundField {
final Field field;
/** Name of the underlying field */
final String fieldName;
+ final TypeToken> resolvedType;
final boolean serialized;
final boolean deserialized;
- protected BoundField(String name, Field field, boolean serialized, boolean deserialized) {
+ protected BoundField(String name, Field field, TypeToken> resolvedType, boolean serialized, boolean deserialized) {
this.name = name;
this.field = field;
this.fieldName = field.getName();
+ this.resolvedType = resolvedType;
this.serialized = serialized;
this.deserialized = deserialized;
}
- /** Read this field value from the source, and append its JSON value to the writer */
+ /** Reads 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 */
+ /** Reads 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 */
+ /** Reads 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;
+
+ /** Puts the field value {@code fieldValue} into the field of {@code target} */
+ abstract void putIntoField(Object fieldValue, Object target) throws IllegalAccessException;
}
/**
@@ -356,10 +375,33 @@ protected BoundField(String name, Field field, boolean serialized, boolean deser
*/
// This class is public because external projects check for this class with `instanceof` (even though it is internal)
public static abstract class Adapter extends TypeAdapter {
+ final Gson gson;
+ final MissingFieldValueStrategy missingFieldValueStrategy;
+ final UnknownFieldStrategy unknownFieldStrategy;
+ final TypeToken type;
final Map boundFields;
-
- Adapter(Map boundFields) {
+ /** Fields to consider for missing field handling; {@code null} if missing fields should be ignored */
+ final Map missingFieldsToCheck;
+
+ Adapter(Gson gson, MissingFieldValueStrategy missingFieldValueStrategy, UnknownFieldStrategy unknownFieldStrategy,
+ TypeToken type, Map boundFields) {
+ this.gson = gson;
+ this.missingFieldValueStrategy = missingFieldValueStrategy;
+ this.unknownFieldStrategy = unknownFieldStrategy;
+ this.type = type;
this.boundFields = boundFields;
+
+ if (missingFieldValueStrategy == MissingFieldValueStrategy.DO_NOTHING) {
+ missingFieldsToCheck = null;
+ } else {
+ // Track the underlying Field because there might be multiple BoundField entries when using @SerializedName
+ missingFieldsToCheck = new LinkedHashMap<>(boundFields.size());
+ for (BoundField boundField : this.boundFields.values()) {
+ if (boundField.deserialized) {
+ missingFieldsToCheck.put(boundField.field, boundField);
+ }
+ }
+ }
}
@Override
@@ -388,6 +430,7 @@ public T read(JsonReader in) throws IOException {
}
A accumulator = createAccumulator();
+ Map missingFields = missingFieldsToCheck == null ? null : new LinkedHashMap<>(missingFieldsToCheck);
try {
in.beginObject();
@@ -395,9 +438,25 @@ public T read(JsonReader in) throws IOException {
String name = in.nextName();
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
- in.skipValue();
+ try {
+ unknownFieldStrategy.handleUnknownField(type, createObjectForFieldStrategy(accumulator), name, in, gson);
+ } catch (IOException e) {
+ // Don't wrap IOException; it is most likely unrelated to unknownFieldStrategy, but instead caused by JSON data
+ throw e;
+ } catch (Exception e) {
+ // UnknownFieldStrategy.THROW_EXCEPTION provides enough context, can directly rethrow
+ if (unknownFieldStrategy == UnknownFieldStrategy.THROW_EXCEPTION) {
+ throw e;
+ }
+ // TODO Proper exception type
+ throw new RuntimeException("Failed handling unknown field '" + name + "' for " + type.getRawType() + " at path " + in.getPath(), e);
+ }
} else {
readField(accumulator, in, field);
+
+ if (missingFields != null) {
+ missingFields.remove(field.field);
+ }
}
}
} catch (IllegalStateException e) {
@@ -406,26 +465,70 @@ public T read(JsonReader in) throws IOException {
throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
}
in.endObject();
+
+ if (missingFields != null && !missingFields.isEmpty()) {
+ for (Map.Entry fieldEntry : missingFields.entrySet()) {
+ Field field = fieldEntry.getKey();
+ BoundField boundField = fieldEntry.getValue();
+ Object newValue;
+ try {
+ newValue = missingFieldValueStrategy.handleMissingField(type, createObjectForFieldStrategy(accumulator), field, boundField.resolvedType);
+ } catch (Exception e) {
+ // TODO Proper exception type
+ throw new RuntimeException("Failed handling missing field '" + ReflectionHelper.fieldToString(field) + "' at path " + in.getPath(), e);
+ }
+
+ // For null values keep the existing initial value
+ if (newValue != null) {
+ try {
+ addMissingFieldValue(accumulator, boundField, newValue);
+ } catch (Exception e) {
+ // TODO Proper exception type
+ throw new RuntimeException("Failed storing " + newValue.getClass().getName() + " provided by " + missingFieldValueStrategy + " into field '" + ReflectionHelper.fieldToString(field) + "' at path " + in.getPreviousPath(), e);
+ }
+ }
+ }
+ }
+
return finalize(accumulator);
}
- /** Create the Object that will be used to collect each field value */
+ /** Creates the Object that will be used to collect each field value */
abstract A createAccumulator();
+
+ /**
+ * Creates the Object based on the accumulator that will be passed as {@code instance}
+ * to the {@link MissingFieldValueStrategy} and {@link UnknownFieldStrategy}.
+ *
+ * @return the object for missing and unknown field strategies, can be {@code null}
+ */
+ abstract Object createObjectForFieldStrategy(A accumulator);
+
/**
- * Read a single BoundField into the accumulator. The JsonReader will be pointed at the
+ * Reads 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. */
+
+ /**
+ * Called for the {@link MissingFieldValueStrategy} to add {@code value} as value
+ * for {@code field}.
+ *
+ * @param value the field value, must not be {@code null}
+ */
+ abstract void addMissingFieldValue(A accumulator, BoundField field, Object value);
+
+ /** Converts the accumulator to a final instance of T. */
abstract T finalize(A accumulator);
}
private static final class FieldReflectionAdapter extends Adapter {
private final ObjectConstructor constructor;
- FieldReflectionAdapter(ObjectConstructor constructor, Map boundFields) {
- super(boundFields);
+ FieldReflectionAdapter(Gson gson, MissingFieldValueStrategy missingFieldValueStrategy, UnknownFieldStrategy unknownFieldStrategy,
+ ObjectConstructor constructor, TypeToken type, Map boundFields) {
+ super(gson, missingFieldValueStrategy, unknownFieldStrategy, type, boundFields);
this.constructor = constructor;
}
@@ -434,12 +537,27 @@ T createAccumulator() {
return constructor.construct();
}
+ @Override
+ Object createObjectForFieldStrategy(T accumulator) {
+ // Let missing and unknown field strategies directly access constructed object
+ return accumulator;
+ }
+
@Override
void readField(T accumulator, JsonReader in, BoundField field)
throws IllegalAccessException, IOException {
field.readIntoField(in, accumulator);
}
+ @Override
+ void addMissingFieldValue(T accumulator, BoundField field, Object value) {
+ try {
+ field.putIntoField(value, accumulator);
+ } catch (IllegalAccessException e) {
+ throw ReflectionHelper.createExceptionForUnexpectedIllegalAccess(e);
+ }
+ }
+
@Override
T finalize(T accumulator) {
return accumulator;
@@ -456,8 +574,9 @@ private static final class RecordAdapter extends Adapter {
// Map from component names to index into the constructors arguments.
private final Map componentIndices = new HashMap<>();
- RecordAdapter(Class raw, Map boundFields, boolean blockInaccessible) {
- super(boundFields);
+ RecordAdapter(Gson gson, MissingFieldValueStrategy missingFieldValueStrategy, UnknownFieldStrategy unknownFieldStrategy,
+ TypeToken type, Class raw, Map boundFields, boolean blockInaccessible) {
+ super(gson, missingFieldValueStrategy, unknownFieldStrategy, type, boundFields);
constructor = ReflectionHelper.getCanonicalRecordConstructor(raw);
if (blockInaccessible) {
@@ -501,19 +620,37 @@ Object[] createAccumulator() {
}
@Override
- void readField(Object[] accumulator, JsonReader in, BoundField field) throws IOException {
+ Object createObjectForFieldStrategy(Object[] accumulator) {
+ // Don't let missing and unknown field strategies directly access internal accumulator object
+ // TODO: In the future maybe provide a Map-like object which encapsulates accumulator,
+ // but restricts operations only to valid component names / property names?
+ return null;
+ }
+
+ private int getComponentIndex(String fieldName) {
// Obtain the component index from the name of the field backing it
- Integer componentIndex = componentIndices.get(field.fieldName);
+ Integer componentIndex = componentIndices.get(fieldName);
if (componentIndex == null) {
throw new IllegalStateException(
"Could not find the index in the constructor '" + ReflectionHelper.constructorToString(constructor) + "'"
- + " for field with name '" + field.fieldName + "',"
+ + " for field with name '" + 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);
+ return componentIndex;
+ }
+
+ @Override
+ void readField(Object[] accumulator, JsonReader in, BoundField field) throws IOException {
+ field.readIntoArray(in, getComponentIndex(field.fieldName), accumulator);
+ }
+
+ @Override
+ void addMissingFieldValue(Object[] accumulator, BoundField field, Object value) {
+ assert(value != null);
+ accumulator[getComponentIndex(field.fieldName)] = value;
}
@Override
diff --git a/gson/src/test/java/com/google/gson/GsonTest.java b/gson/src/test/java/com/google/gson/GsonTest.java
index c1e9e9d785..35259ffc21 100644
--- a/gson/src/test/java/com/google/gson/GsonTest.java
+++ b/gson/src/test/java/com/google/gson/GsonTest.java
@@ -68,6 +68,7 @@ public void testOverridesDefaultExcluder() {
DateFormat.DEFAULT, new ArrayList(),
new ArrayList(), new ArrayList(),
CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY,
+ MissingFieldValueStrategy.THROW_EXCEPTION, UnknownFieldStrategy.THROW_EXCEPTION,
Collections.emptyList());
assertThat(gson.excluder).isEqualTo(CUSTOM_EXCLUDER);
@@ -85,6 +86,7 @@ public void testClonedTypeAdapterFactoryListsAreIndependent() {
DateFormat.DEFAULT, new ArrayList(),
new ArrayList(), new ArrayList(),
CUSTOM_OBJECT_TO_NUMBER_STRATEGY, CUSTOM_NUMBER_TO_NUMBER_STRATEGY,
+ MissingFieldValueStrategy.THROW_EXCEPTION, UnknownFieldStrategy.THROW_EXCEPTION,
Collections.emptyList());
Gson clone = original.newBuilder()
diff --git a/gson/src/test/java/com/google/gson/Java17MissingFieldValueStrategyTest.java b/gson/src/test/java/com/google/gson/Java17MissingFieldValueStrategyTest.java
new file mode 100644
index 0000000000..dfcb655eeb
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/Java17MissingFieldValueStrategyTest.java
@@ -0,0 +1,197 @@
+package com.google.gson;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+
+public class Java17MissingFieldValueStrategyTest {
+ @Test
+ public void testDoNothing() {
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(MissingFieldValueStrategy.DO_NOTHING).create();
+
+ CustomRecord deserialized = gson.fromJson("{\"a\": \"custom-a\"}", CustomRecord.class);
+ assertThat(deserialized.a).isEqualTo("custom-a");
+ assertThat(deserialized.b).isEqualTo(null);
+ }
+
+ @Test
+ public void testThrowException() {
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(MissingFieldValueStrategy.THROW_EXCEPTION).create();
+
+ CustomRecord deserialized = gson.fromJson("{\"a\": \"custom-a\", \"b\": \"custom-b\"}", CustomRecord.class);
+ assertThat(deserialized.a).isEqualTo("custom-a");
+ assertThat(deserialized.b).isEqualTo("custom-b");
+
+ try {
+ gson.fromJson("{\"a\": \"custom-a\"}", CustomRecord.class);
+ fail();
+ }
+ // TODO: Adjust this once a more specific exception is thrown
+ catch (RuntimeException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Failed handling missing field '" + CustomRecord.class.getName() + "#b' at path $");
+ assertThat(expected).hasCauseThat().hasMessageThat().isEqualTo("Missing value for field '" + CustomRecord.class.getName() + "#b'");
+ }
+
+ try {
+ gson.fromJson("{\"b\": \"custom-b\"}", CustomRecord.class);
+ fail();
+ }
+ // TODO: Adjust this once a more specific exception is thrown
+ catch (RuntimeException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Failed handling missing field '" + CustomRecord.class.getName() + "#a' at path $");
+ assertThat(expected).hasCauseThat().hasMessageThat().isEqualTo("Missing value for field '" + CustomRecord.class.getName() + "#a'");
+ }
+ }
+
+
+ /**
+ * Should only report missing field once, even if {@code @SerializedName} specifies multiple names.
+ */
+ @Test
+ public void testSerializedName() throws Exception {
+ List missingFields = new ArrayList<>();
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field,
+ TypeToken> resolvedFieldType) {
+ missingFields.add(field);
+ return 1;
+ }
+ }).create();
+
+ WithSerializedName deserialized = gson.fromJson("{}", WithSerializedName.class);
+ assertThat(deserialized.a).isEqualTo(1);
+ Field field = WithSerializedName.class.getDeclaredField("a");
+ assertThat(missingFields).containsExactly(field);
+ }
+
+ /**
+ * Should handle serialization and deserialization exclusions correctly.
+ */
+ @Test
+ public void testExcluded() throws Exception {
+ List missingFields = new ArrayList<>();
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field,
+ TypeToken> resolvedFieldType) {
+ missingFields.add(field);
+ return null;
+ }
+ }).excludeFieldsWithoutExposeAnnotation().create();
+
+ gson.fromJson("{}", WithExclusions.class);
+ Field field1 = WithExclusions.class.getDeclaredField("both");
+ Field field2 = WithExclusions.class.getDeclaredField("deserialize");
+ assertThat(missingFields).containsExactly(field1, field2);
+ }
+
+ @Test
+ public void testResolvedFieldType() {
+ List> fieldTypes = new ArrayList<>();
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field,
+ TypeToken> resolvedFieldType) {
+ fieldTypes.add(resolvedFieldType);
+ return null;
+ }
+ }).create();
+
+ gson.fromJson("{}", new TypeToken>() {});
+ assertThat(fieldTypes).containsExactly(TypeToken.get(String.class));
+ }
+
+ @Test
+ public void testCustom() {
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field, TypeToken> resolvedFieldType) {
+ assertThat(declaringType).isEqualTo(TypeToken.get(CustomRecord.class));
+ // Due to how Record instances are constructed currently cannot provide access to instance
+ assertThat(instance).isNull();
+ assertThat(field.getDeclaringClass()).isEqualTo(CustomRecord.class);
+
+ if (field.getName().equals("a")) {
+ // Preserve existing value
+ return null;
+ }
+ return "field-" + field.getName();
+ }
+ }).create();
+
+ CustomRecord deserialized = gson.fromJson("{}", CustomRecord.class);
+ assertThat(deserialized.a).isEqualTo(null);
+ assertThat(deserialized.b).isEqualTo("field-b");
+
+
+ gson = new GsonBuilder().setMissingFieldValueStrategy(new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field, TypeToken> resolvedFieldType) {
+ // Preserve existing value
+ return null;
+ }
+ }).create();
+ RecordWithInt deserialized2 = gson.fromJson("{}", RecordWithInt.class);
+ // Uses default value for primitive
+ assertThat(deserialized2.a).isEqualTo(0);
+ }
+
+ @Test
+ public void testBadNewFieldValue() {
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field, TypeToken> resolvedFieldType) {
+ return 1;
+ }
+
+ @Override
+ public String toString() {
+ return "my-strategy";
+ }
+ }).create();
+
+ try {
+ gson.fromJson("{\"a\": \"custom-a\"}", CustomRecord.class);
+ fail();
+ }
+ // TODO: Adjust this once a more specific exception is thrown
+ catch (RuntimeException expected) {
+ // Exception currently does not point out usage of MissingFieldValueStrategy
+ assertThat(expected).hasMessageThat().isEqualTo("Failed to invoke constructor 'com.google.gson.Java17MissingFieldValueStrategyTest$CustomRecord(String, String)'"
+ + " with args [custom-a, 1]");
+ assertThat(expected).hasCauseThat().isNotNull();
+ }
+ }
+
+ record CustomRecord(String a, String b) {}
+
+ record RecordWithInt(int a) {}
+
+ record WithSerializedName(
+ @SerializedName(value = "b", alternate = {"c", "d", "e"})
+ int a
+ ) {}
+
+ record WithExclusions(
+ @Expose(deserialize = true, serialize = true)
+ int both,
+ @Expose(deserialize = true, serialize = false)
+ int deserialize,
+ @Expose(deserialize = false, serialize = true)
+ int serialize,
+ @Expose(deserialize = false, serialize = false)
+ int none
+ ) {}
+
+ record WithTypeVariable(
+ T a
+ ) {}
+}
diff --git a/gson/src/test/java/com/google/gson/Java17UnknownFieldStrategyTest.java b/gson/src/test/java/com/google/gson/Java17UnknownFieldStrategyTest.java
new file mode 100644
index 0000000000..2262fe14d8
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/Java17UnknownFieldStrategyTest.java
@@ -0,0 +1,84 @@
+package com.google.gson;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.junit.Test;
+
+public class Java17UnknownFieldStrategyTest {
+ @Test
+ public void testIgnore() {
+ Gson gson = new GsonBuilder().setUnknownFieldStrategy(UnknownFieldStrategy.IGNORE).create();
+
+ CustomRecord deserialized = gson.fromJson("{\"a\": 1, \"b\": 2}", CustomRecord.class);
+ assertThat(deserialized.a).isEqualTo(1);
+ }
+
+ @Test
+ public void testThrowException() {
+ Gson gson = new GsonBuilder().setUnknownFieldStrategy(UnknownFieldStrategy.THROW_EXCEPTION).create();
+
+ CustomRecord deserialized = gson.fromJson("{\"a\": 1}", CustomRecord.class);
+ assertThat(deserialized.a).isEqualTo(1);
+
+ try {
+ gson.fromJson("{\"a\": 1, \"b\": 2}", CustomRecord.class);
+ fail();
+ }
+ // TODO: Adjust this once a more specific exception is thrown
+ catch (RuntimeException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Unknown field 'b' for " + CustomRecord.class + " at path $.b");
+ }
+ }
+
+ @Test
+ public void testCustomThrowing() {
+ Gson gson = new GsonBuilder().setUnknownFieldStrategy(new UnknownFieldStrategy() {
+ @Override
+ public void handleUnknownField(TypeToken> declaringType, Object instance, String fieldName,
+ JsonReader jsonReader, Gson gson) throws IOException {
+ throw new RuntimeException("my-exception");
+ }
+ }).create();
+
+ try {
+ gson.fromJson("{\"a\": 1, \"b\": 2}", CustomRecord.class);
+ fail();
+ }
+ // TODO: Adjust this once a more specific exception is thrown
+ catch (RuntimeException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Failed handling unknown field 'b' for " + CustomRecord.class + " at path $.b");
+ assertThat(expected).hasCauseThat().hasMessageThat().isEqualTo("my-exception");
+ }
+ }
+
+ @Test
+ public void testCustom() {
+ Map unknownFieldsMap = new LinkedHashMap<>();
+ Gson gson = new GsonBuilder().setUnknownFieldStrategy(new UnknownFieldStrategy() {
+ @Override
+ public void handleUnknownField(TypeToken> declaringType, Object instance, String fieldName,
+ JsonReader jsonReader, Gson gson) throws IOException {
+ assertThat(declaringType).isEqualTo(TypeToken.get(CustomRecord.class));
+ // Due to how Record instances are constructed currently cannot provide access to instance
+ assertThat(instance).isNull();
+ assertThat(jsonReader).isNotNull();
+ assertThat(gson).isNotNull();
+
+ Object value = gson.fromJson(jsonReader, Object.class);
+ unknownFieldsMap.put(fieldName, value);
+ }
+ }).create();
+
+ CustomRecord deserialized = gson.fromJson("{\"a\": 1, \"b\": 2}", CustomRecord.class);
+ assertThat(deserialized.a).isEqualTo(1);
+ assertThat(unknownFieldsMap).containsExactly("b", 2.0);
+ }
+
+ record CustomRecord(int a) { }
+}
diff --git a/gson/src/test/java/com/google/gson/MissingFieldValueStrategyTest.java b/gson/src/test/java/com/google/gson/MissingFieldValueStrategyTest.java
new file mode 100644
index 0000000000..edd63cd53e
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/MissingFieldValueStrategyTest.java
@@ -0,0 +1,201 @@
+package com.google.gson;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.annotations.SerializedName;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+
+public class MissingFieldValueStrategyTest {
+ @Test
+ public void testDoNothing() {
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(MissingFieldValueStrategy.DO_NOTHING).create();
+
+ CustomClass deserialized = gson.fromJson("{\"a\": \"custom-a\"}", CustomClass.class);
+ assertThat(deserialized.a).isEqualTo("custom-a");
+ assertThat(deserialized.b).isEqualTo("default-b");
+ }
+
+ @Test
+ public void testThrowException() {
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(MissingFieldValueStrategy.THROW_EXCEPTION).create();
+
+ CustomClass deserialized = gson.fromJson("{\"a\": \"custom-a\", \"b\": \"custom-b\"}", CustomClass.class);
+ assertThat(deserialized.a).isEqualTo("custom-a");
+ assertThat(deserialized.b).isEqualTo("custom-b");
+
+ try {
+ gson.fromJson("{\"a\": \"custom-a\"}", CustomClass.class);
+ fail();
+ }
+ // TODO: Adjust this once a more specific exception is thrown
+ catch (RuntimeException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Failed handling missing field '" + CustomClass.class.getName() + "#b' at path $");
+ assertThat(expected).hasCauseThat().hasMessageThat().isEqualTo("Missing value for field '" + CustomClass.class.getName() + "#b'");
+ }
+
+ try {
+ gson.fromJson("{\"b\": \"custom-b\"}", CustomClass.class);
+ fail();
+ }
+ // TODO: Adjust this once a more specific exception is thrown
+ catch (RuntimeException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Failed handling missing field '" + CustomClass.class.getName() + "#a' at path $");
+ assertThat(expected).hasCauseThat().hasMessageThat().isEqualTo("Missing value for field '" + CustomClass.class.getName() + "#a'");
+ }
+ }
+
+ /**
+ * Should only report missing field once, even if {@code @SerializedName} specifies multiple names.
+ */
+ @Test
+ public void testSerializedName() throws Exception {
+ List missingFields = new ArrayList<>();
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field,
+ TypeToken> resolvedFieldType) {
+ missingFields.add(field);
+ return 1;
+ }
+ }).create();
+
+ WithSerializedName deserialized = gson.fromJson("{}", WithSerializedName.class);
+ assertThat(deserialized.a).isEqualTo(1);
+ Field field = WithSerializedName.class.getDeclaredField("a");
+ assertThat(missingFields).containsExactly(field);
+ }
+
+ /**
+ * Should handle serialization and deserialization exclusions correctly.
+ */
+ @Test
+ public void testExcluded() throws Exception {
+ List missingFields = new ArrayList<>();
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field,
+ TypeToken> resolvedFieldType) {
+ missingFields.add(field);
+ return null;
+ }
+ }).excludeFieldsWithoutExposeAnnotation().create();
+
+ gson.fromJson("{}", WithExclusions.class);
+ Field field1 = WithExclusions.class.getDeclaredField("both");
+ Field field2 = WithExclusions.class.getDeclaredField("deserialize");
+ assertThat(missingFields).containsExactly(field1, field2);
+ }
+
+ @Test
+ public void testResolvedFieldType() {
+ List> fieldTypes = new ArrayList<>();
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field,
+ TypeToken> resolvedFieldType) {
+ fieldTypes.add(resolvedFieldType);
+ return null;
+ }
+ }).create();
+
+ gson.fromJson("{}", new TypeToken>() {});
+ assertThat(fieldTypes).containsExactly(TypeToken.get(String.class));
+ }
+
+ @Test
+ public void testCustom() {
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field, TypeToken> resolvedFieldType) {
+ assertThat(declaringType).isEqualTo(TypeToken.get(CustomClass.class));
+ assertThat(instance).isInstanceOf(CustomClass.class);
+ assertThat(field.getDeclaringClass()).isEqualTo(CustomClass.class);
+
+ try {
+ Object existingValue = field.get(instance);
+ return "field-" + field.getName() + "-" + existingValue;
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }).create();
+
+ CustomClass deserialized = gson.fromJson("{}", CustomClass.class);
+ assertThat(deserialized.a).isEqualTo("field-a-default-a");
+ assertThat(deserialized.b).isEqualTo("field-b-default-b");
+
+
+ gson = new GsonBuilder().setMissingFieldValueStrategy(new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field, TypeToken> resolvedFieldType) {
+ if (field.getName().equals("a")) {
+ // Preserve existing value
+ return null;
+ }
+ return "field-" + field.getName();
+ }
+ }).create();
+
+ deserialized = gson.fromJson("{}", CustomClass.class);
+ assertThat(deserialized.a).isEqualTo("default-a");
+ assertThat(deserialized.b).isEqualTo("field-b");
+ }
+
+ @Test
+ public void testBadNewFieldValue() {
+ Gson gson = new GsonBuilder().setMissingFieldValueStrategy(new MissingFieldValueStrategy() {
+ @Override
+ public Object handleMissingField(TypeToken> declaringType, Object instance, Field field, TypeToken> resolvedFieldType) {
+ return 1;
+ }
+
+ @Override
+ public String toString() {
+ return "my-strategy";
+ }
+ }).create();
+
+ try {
+ gson.fromJson("{\"a\": \"custom-a\"}", CustomClass.class);
+ fail();
+ }
+ // TODO: Adjust this once a more specific exception is thrown
+ catch (RuntimeException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Failed storing java.lang.Integer provided by my-strategy"
+ + " into field '" + CustomClass.class.getName() + "#b' at path $");
+ assertThat(expected).hasCauseThat().isNotNull();
+ }
+ }
+
+ private static class CustomClass {
+ String a = "default-a";
+ String b = "default-b";
+ }
+
+ static class WithSerializedName {
+ @SerializedName(value = "b", alternate = {"c", "d", "e"})
+ int a;
+ }
+
+ static class WithExclusions {
+ @Expose(deserialize = true, serialize = true)
+ int both;
+ @Expose(deserialize = true, serialize = false)
+ int deserialize;
+ @Expose(deserialize = false, serialize = true)
+ int serialize;
+ @Expose(deserialize = false, serialize = false)
+ int none;
+ }
+
+ private static class WithTypeVariable {
+ @SuppressWarnings("unused")
+ T a;
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/UnknownFieldStrategyTest.java b/gson/src/test/java/com/google/gson/UnknownFieldStrategyTest.java
new file mode 100644
index 0000000000..5be0fb993c
--- /dev/null
+++ b/gson/src/test/java/com/google/gson/UnknownFieldStrategyTest.java
@@ -0,0 +1,163 @@
+package com.google.gson;
+
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.gson.annotations.Expose;
+import com.google.gson.reflect.TypeToken;
+import com.google.gson.stream.JsonReader;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.Test;
+
+public class UnknownFieldStrategyTest {
+ @Test
+ public void testIgnore() {
+ Gson gson = new GsonBuilder().setUnknownFieldStrategy(UnknownFieldStrategy.IGNORE).create();
+
+ CustomClass deserialized = gson.fromJson("{\"a\": 1, \"b\": 2}", CustomClass.class);
+ assertThat(deserialized.a).isEqualTo(1);
+ assertThat(deserialized.unknownFields).isEmpty();
+ }
+
+ @Test
+ public void testThrowException() {
+ Gson gson = new GsonBuilder().setUnknownFieldStrategy(UnknownFieldStrategy.THROW_EXCEPTION).create();
+
+ CustomClass deserialized = gson.fromJson("{\"a\": 1}", CustomClass.class);
+ assertThat(deserialized.a).isEqualTo(1);
+ assertThat(deserialized.unknownFields).isEmpty();
+
+ try {
+ gson.fromJson("{\"a\": 1, \"b\": 2}", CustomClass.class);
+ fail();
+ }
+ // TODO: Adjust this once a more specific exception is thrown
+ catch (RuntimeException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Unknown field 'b' for " + CustomClass.class + " at path $.b");
+ }
+ }
+
+ @Test
+ public void testCustomThrowing() {
+ Gson gson = new GsonBuilder().setUnknownFieldStrategy(new UnknownFieldStrategy() {
+ @Override
+ public void handleUnknownField(TypeToken> declaringType, Object instance, String fieldName,
+ JsonReader jsonReader, Gson gson) throws IOException {
+ throw new RuntimeException("my-exception");
+ }
+ }).create();
+
+ try {
+ gson.fromJson("{\"a\": 1, \"b\": 2}", CustomClass.class);
+ fail();
+ }
+ // TODO: Adjust this once a more specific exception is thrown
+ catch (RuntimeException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Failed handling unknown field 'b' for " + CustomClass.class + " at path $.b");
+ assertThat(expected).hasCauseThat().hasMessageThat().isEqualTo("my-exception");
+ }
+ }
+
+ @Test
+ public void testCustomThrowingAfterRead() {
+ Gson gson = new GsonBuilder().setUnknownFieldStrategy(new UnknownFieldStrategy() {
+ @Override
+ public void handleUnknownField(TypeToken> declaringType, Object instance, String fieldName,
+ JsonReader jsonReader, Gson gson) throws IOException {
+ // Consume the value before throwing exception
+ jsonReader.skipValue();
+ throw new RuntimeException("my-exception");
+ }
+ }).create();
+
+ try {
+ gson.fromJson("{\"a\": 1, \"b\": 2}", CustomClass.class);
+ fail();
+ }
+ // TODO: Adjust this once a more specific exception is thrown
+ catch (RuntimeException expected) {
+ assertThat(expected).hasMessageThat().isEqualTo("Failed handling unknown field 'b' for " + CustomClass.class + " at path $.b");
+ assertThat(expected).hasCauseThat().hasMessageThat().isEqualTo("my-exception");
+ }
+ }
+
+ /**
+ * Provides a simple example for how to store unknown values in an extra field on the class.
+ *
+ * Important: Do not use this code in production; it is not properly handling the
+ * case where no such field exists, or when the deserialized class is a Record.
+ */
+ @Test
+ public void testCustom() {
+ Gson gson = new GsonBuilder().setUnknownFieldStrategy(new UnknownFieldStrategy() {
+ @Override
+ public void handleUnknownField(TypeToken> declaringType, Object instance, String fieldName,
+ JsonReader jsonReader, Gson gson) throws IOException {
+ assertThat(declaringType).isEqualTo(TypeToken.get(CustomClass.class));
+ assertThat(instance).isInstanceOf(CustomClass.class);
+ assertThat(jsonReader).isNotNull();
+ assertThat(gson).isNotNull();
+
+ try {
+ Field unknownFieldsField = declaringType.getRawType().getDeclaredField("unknownFields");
+
+ @SuppressWarnings("unchecked")
+ Map unknownFieldsMap = (Map) unknownFieldsField.get(instance);
+ if (unknownFieldsMap.containsKey(fieldName)) {
+ throw new IllegalArgumentException("Already contains value for " + fieldName);
+ }
+
+ Object value = gson.fromJson(jsonReader, Object.class);
+ unknownFieldsMap.put(fieldName, value);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }).create();
+
+ CustomClass deserialized = gson.fromJson("{\"a\": 1, \"b\": 2}", CustomClass.class);
+ assertThat(deserialized.a).isEqualTo(1);
+ assertThat(deserialized.unknownFields).containsExactly("b", 2.0);
+ }
+
+ @Test
+ public void testExcludedConsideredUnknown() {
+ List unknownFields = new ArrayList<>();
+ Gson gson = new GsonBuilder().setUnknownFieldStrategy(new UnknownFieldStrategy() {
+ @Override
+ public void handleUnknownField(TypeToken> declaringType, Object instance, String fieldName,
+ JsonReader jsonReader, Gson gson) throws IOException {
+ jsonReader.skipValue();
+ unknownFields.add(fieldName);
+ }
+ }).excludeFieldsWithoutExposeAnnotation().create();
+
+ WithExcluded obj = new WithExcluded();
+ obj.a = 1;
+ String json = gson.toJson(obj);
+ // Serialization should include field
+ assertThat(json).isEqualTo("{\"a\":1}");
+
+ WithExcluded deserialized = gson.fromJson(json, WithExcluded.class);
+ assertThat(deserialized.a).isEqualTo(0);
+ // Excluded field should be considered unknown
+ assertThat(unknownFields).containsExactly("a");
+ }
+
+ private static class CustomClass {
+ int a;
+
+ transient Map unknownFields = new LinkedHashMap<>();
+ }
+
+ private static class WithExcluded {
+ @Expose(deserialize = false, serialize = true)
+ int a;
+ }
+}
diff --git a/gson/src/test/java/com/google/gson/functional/ArrayTest.java b/gson/src/test/java/com/google/gson/functional/ArrayTest.java
index b88eda578e..570eef4b29 100644
--- a/gson/src/test/java/com/google/gson/functional/ArrayTest.java
+++ b/gson/src/test/java/com/google/gson/functional/ArrayTest.java
@@ -28,7 +28,6 @@
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
import org.junit.Before;
import org.junit.Test;
diff --git a/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java b/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java
index 5496065119..de3ca47c51 100644
--- a/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java
+++ b/gson/src/test/java/com/google/gson/functional/PrettyPrintingTest.java
@@ -26,7 +26,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.junit.Before;
@@ -92,7 +91,7 @@ public void testPrettyPrintListOfPrimitiveArrays() {
assertThat(json).isEqualTo("[\n [\n 1,\n 2\n ],\n [\n 3,\n 4\n ],\n [\n 5,\n 6\n ],"
+ "\n [\n 7,\n 8\n ],\n [\n 9,\n 0\n ],\n [\n 10\n ]\n]");
}
-
+
@Test
public void testMap() {
Map map = new LinkedHashMap<>();
diff --git a/gson/src/test/java/com/google/gson/functional/UncategorizedTest.java b/gson/src/test/java/com/google/gson/functional/UncategorizedTest.java
index 358fc5a9a1..8ec5b4af91 100644
--- a/gson/src/test/java/com/google/gson/functional/UncategorizedTest.java
+++ b/gson/src/test/java/com/google/gson/functional/UncategorizedTest.java
@@ -28,7 +28,6 @@
import com.google.gson.common.TestTypes.ClassOverridingEquals;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
-import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;