Skip to content

Commit

Permalink
Add strategies for unknown and missing fields
Browse files Browse the repository at this point in the history
  • Loading branch information
Marcono1234 committed Mar 26, 2023
1 parent c34b9ff commit 140eda7
Show file tree
Hide file tree
Showing 14 changed files with 997 additions and 30 deletions.
11 changes: 10 additions & 1 deletion gson/src/main/java/com/google/gson/Gson.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -195,6 +197,8 @@ public final class Gson {
final List<TypeAdapterFactory> builderHierarchyFactories;
final ToNumberStrategy objectToNumberStrategy;
final ToNumberStrategy numberToNumberStrategy;
final MissingFieldValueStrategy missingFieldValueStrategy;
final UnknownFieldStrategy unknownFieldStrategy;
final List<ReflectionAccessFilter> reflectionFilters;

/**
Expand Down Expand Up @@ -242,6 +246,7 @@ public Gson() {
LongSerializationPolicy.DEFAULT, DEFAULT_DATE_PATTERN, DateFormat.DEFAULT, DateFormat.DEFAULT,
Collections.<TypeAdapterFactory>emptyList(), Collections.<TypeAdapterFactory>emptyList(),
Collections.<TypeAdapterFactory>emptyList(), DEFAULT_OBJECT_TO_NUMBER_STRATEGY, DEFAULT_NUMBER_TO_NUMBER_STRATEGY,
DEFAULT_MISSING_FIELD_VALUE_STRATEGY, DEFAULT_UNKNOWN_FIELD_STRATEGY,
Collections.<ReflectionAccessFilter>emptyList());
}

Expand All @@ -255,6 +260,7 @@ public Gson() {
List<TypeAdapterFactory> builderHierarchyFactories,
List<TypeAdapterFactory> factoriesToBeAdded,
ToNumberStrategy objectToNumberStrategy, ToNumberStrategy numberToNumberStrategy,
MissingFieldValueStrategy missingFieldValueStrategy, UnknownFieldStrategy unknownFieldStrategy,
List<ReflectionAccessFilter> reflectionFilters) {
this.excluder = excluder;
this.fieldNamingStrategy = fieldNamingStrategy;
Expand All @@ -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<TypeAdapterFactory> factories = new ArrayList<>();
Expand Down Expand Up @@ -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);
}
Expand Down
45 changes: 41 additions & 4 deletions gson/src/main/java/com/google/gson/GsonBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.</p>
*
* <p>The following is an example shows how to use the {@code GsonBuilder} to construct a Gson
* <p>The following example shows how to use the {@code GsonBuilder} to construct a Gson
* instance:
*
* <pre>
Expand All @@ -73,8 +75,8 @@
*
* <p>NOTES:
* <ul>
* <li> the order of invocation of configuration methods does not matter.</li>
* <li> The default serialization of {@link Date} and its subclasses in Gson does
* <li>the order of invocation of configuration methods does not matter.</li>
* <li>the default serialization of {@link Date} and its subclasses in Gson does
* not contain time-zone information. So, if you are using date/time instances,
* use {@code GsonBuilder} and its {@code setDateFormat} methods.</li>
* </ul>
Expand Down Expand Up @@ -104,6 +106,8 @@ public final class GsonBuilder {
private boolean useJdkUnsafe = DEFAULT_USE_JDK_UNSAFE;
private ToNumberStrategy objectToNumberStrategy = DEFAULT_OBJECT_TO_NUMBER_STRATEGY;
private ToNumberStrategy numberToNumberStrategy = DEFAULT_NUMBER_TO_NUMBER_STRATEGY;
private MissingFieldValueStrategy missingFieldValueStrategy = DEFAULT_MISSING_FIELD_VALUE_STRATEGY;
private UnknownFieldStrategy unknownFieldStrategy = DEFAULT_UNKNOWN_FIELD_STRATEGY;
private final ArrayDeque<ReflectionAccessFilter> reflectionFilters = new ArrayDeque<>();

/**
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -388,6 +394,36 @@ 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.
*
* @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}.
*
Expand Down Expand Up @@ -782,7 +818,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,
Expand Down
77 changes: 77 additions & 0 deletions gson/src/main/java/com/google/gson/MissingFieldValueStrategy.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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.<br>
* For Record classes (Java 16 feature) the {@code instance} is {@code null}.
*
* <p>{@code resolvedFieldType} is the type of the field with type variables being resolved, if
* possible. For example if {@code class MyClass<T>} has a field {@code T myField} and
* {@code MyClass<String>} 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);
}
16 changes: 16 additions & 0 deletions gson/src/main/java/com/google/gson/ReflectionAccessFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ enum FilterResult {
? FilterResult.BLOCK_INACCESSIBLE
: FilterResult.INDECISIVE;
}

@Override public String toString() {
return "ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA";
}
};

/**
Expand All @@ -149,6 +153,10 @@ enum FilterResult {
? FilterResult.BLOCK_ALL
: FilterResult.INDECISIVE;
}

@Override public String toString() {
return "ReflectionAccessFilter.BLOCK_ALL_JAVA";
}
};

/**
Expand All @@ -173,6 +181,10 @@ enum FilterResult {
? FilterResult.BLOCK_ALL
: FilterResult.INDECISIVE;
}

@Override public String toString() {
return "ReflectionAccessFilter.BLOCK_ALL_ANDROID";
}
};

/**
Expand All @@ -198,6 +210,10 @@ enum FilterResult {
? FilterResult.BLOCK_ALL
: FilterResult.INDECISIVE;
}

@Override public String toString() {
return "ReflectionAccessFilter.BLOCK_ALL_PLATFORM";
}
};

/**
Expand Down
79 changes: 79 additions & 0 deletions gson/src/main/java/com/google/gson/UnknownFieldStrategy.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p><b>Note:</b> 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.
*
* <p>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()}.<br>
* 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.
*
* <p>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<String, Object>} field for all unknown values.<br>
* 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;
}
Loading

0 comments on commit 140eda7

Please sign in to comment.