Skip to content

Commit 140eda7

Browse files
committed
Add strategies for unknown and missing fields
1 parent c34b9ff commit 140eda7

14 files changed

+997
-30
lines changed

gson/src/main/java/com/google/gson/Gson.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ public final class Gson {
151151
static final FieldNamingStrategy DEFAULT_FIELD_NAMING_STRATEGY = FieldNamingPolicy.IDENTITY;
152152
static final ToNumberStrategy DEFAULT_OBJECT_TO_NUMBER_STRATEGY = ToNumberPolicy.DOUBLE;
153153
static final ToNumberStrategy DEFAULT_NUMBER_TO_NUMBER_STRATEGY = ToNumberPolicy.LAZILY_PARSED_NUMBER;
154+
static final MissingFieldValueStrategy DEFAULT_MISSING_FIELD_VALUE_STRATEGY = MissingFieldValueStrategy.DO_NOTHING;
155+
static final UnknownFieldStrategy DEFAULT_UNKNOWN_FIELD_STRATEGY = UnknownFieldStrategy.IGNORE;
154156

155157
private static final String JSON_NON_EXECUTABLE_PREFIX = ")]}'\n";
156158

@@ -195,6 +197,8 @@ public final class Gson {
195197
final List<TypeAdapterFactory> builderHierarchyFactories;
196198
final ToNumberStrategy objectToNumberStrategy;
197199
final ToNumberStrategy numberToNumberStrategy;
200+
final MissingFieldValueStrategy missingFieldValueStrategy;
201+
final UnknownFieldStrategy unknownFieldStrategy;
198202
final List<ReflectionAccessFilter> reflectionFilters;
199203

200204
/**
@@ -242,6 +246,7 @@ public Gson() {
242246
LongSerializationPolicy.DEFAULT, DEFAULT_DATE_PATTERN, DateFormat.DEFAULT, DateFormat.DEFAULT,
243247
Collections.<TypeAdapterFactory>emptyList(), Collections.<TypeAdapterFactory>emptyList(),
244248
Collections.<TypeAdapterFactory>emptyList(), DEFAULT_OBJECT_TO_NUMBER_STRATEGY, DEFAULT_NUMBER_TO_NUMBER_STRATEGY,
249+
DEFAULT_MISSING_FIELD_VALUE_STRATEGY, DEFAULT_UNKNOWN_FIELD_STRATEGY,
245250
Collections.<ReflectionAccessFilter>emptyList());
246251
}
247252

@@ -255,6 +260,7 @@ public Gson() {
255260
List<TypeAdapterFactory> builderHierarchyFactories,
256261
List<TypeAdapterFactory> factoriesToBeAdded,
257262
ToNumberStrategy objectToNumberStrategy, ToNumberStrategy numberToNumberStrategy,
263+
MissingFieldValueStrategy missingFieldValueStrategy, UnknownFieldStrategy unknownFieldStrategy,
258264
List<ReflectionAccessFilter> reflectionFilters) {
259265
this.excluder = excluder;
260266
this.fieldNamingStrategy = fieldNamingStrategy;
@@ -276,6 +282,8 @@ public Gson() {
276282
this.builderHierarchyFactories = builderHierarchyFactories;
277283
this.objectToNumberStrategy = objectToNumberStrategy;
278284
this.numberToNumberStrategy = numberToNumberStrategy;
285+
this.missingFieldValueStrategy = missingFieldValueStrategy;
286+
this.unknownFieldStrategy = unknownFieldStrategy;
279287
this.reflectionFilters = reflectionFilters;
280288

281289
List<TypeAdapterFactory> factories = new ArrayList<>();
@@ -341,7 +349,8 @@ public Gson() {
341349
factories.add(jsonAdapterFactory);
342350
factories.add(TypeAdapters.ENUM_FACTORY);
343351
factories.add(new ReflectiveTypeAdapterFactory(
344-
constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory, reflectionFilters));
352+
constructorConstructor, fieldNamingStrategy, excluder, jsonAdapterFactory,
353+
missingFieldValueStrategy, unknownFieldStrategy, reflectionFilters));
345354

346355
this.factories = Collections.unmodifiableList(factories);
347356
}

gson/src/main/java/com/google/gson/GsonBuilder.java

+41-4
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
import static com.google.gson.Gson.DEFAULT_FORMATTING_STYLE;
2323
import static com.google.gson.Gson.DEFAULT_JSON_NON_EXECUTABLE;
2424
import static com.google.gson.Gson.DEFAULT_LENIENT;
25+
import static com.google.gson.Gson.DEFAULT_MISSING_FIELD_VALUE_STRATEGY;
2526
import static com.google.gson.Gson.DEFAULT_NUMBER_TO_NUMBER_STRATEGY;
2627
import static com.google.gson.Gson.DEFAULT_OBJECT_TO_NUMBER_STRATEGY;
2728
import static com.google.gson.Gson.DEFAULT_SERIALIZE_NULLS;
2829
import static com.google.gson.Gson.DEFAULT_SPECIALIZE_FLOAT_VALUES;
30+
import static com.google.gson.Gson.DEFAULT_UNKNOWN_FIELD_STRATEGY;
2931
import static com.google.gson.Gson.DEFAULT_USE_JDK_UNSAFE;
3032

3133
import com.google.gson.annotations.Since;
@@ -56,7 +58,7 @@
5658
* use {@code new Gson()}. {@code GsonBuilder} is best used by creating it, and then invoking its
5759
* various configuration methods, and finally calling create.</p>
5860
*
59-
* <p>The following is an example shows how to use the {@code GsonBuilder} to construct a Gson
61+
* <p>The following example shows how to use the {@code GsonBuilder} to construct a Gson
6062
* instance:
6163
*
6264
* <pre>
@@ -73,8 +75,8 @@
7375
*
7476
* <p>NOTES:
7577
* <ul>
76-
* <li> the order of invocation of configuration methods does not matter.</li>
77-
* <li> The default serialization of {@link Date} and its subclasses in Gson does
78+
* <li>the order of invocation of configuration methods does not matter.</li>
79+
* <li>the default serialization of {@link Date} and its subclasses in Gson does
7880
* not contain time-zone information. So, if you are using date/time instances,
7981
* use {@code GsonBuilder} and its {@code setDateFormat} methods.</li>
8082
* </ul>
@@ -104,6 +106,8 @@ public final class GsonBuilder {
104106
private boolean useJdkUnsafe = DEFAULT_USE_JDK_UNSAFE;
105107
private ToNumberStrategy objectToNumberStrategy = DEFAULT_OBJECT_TO_NUMBER_STRATEGY;
106108
private ToNumberStrategy numberToNumberStrategy = DEFAULT_NUMBER_TO_NUMBER_STRATEGY;
109+
private MissingFieldValueStrategy missingFieldValueStrategy = DEFAULT_MISSING_FIELD_VALUE_STRATEGY;
110+
private UnknownFieldStrategy unknownFieldStrategy = DEFAULT_UNKNOWN_FIELD_STRATEGY;
107111
private final ArrayDeque<ReflectionAccessFilter> reflectionFilters = new ArrayDeque<>();
108112

109113
/**
@@ -141,6 +145,8 @@ public GsonBuilder() {
141145
this.useJdkUnsafe = gson.useJdkUnsafe;
142146
this.objectToNumberStrategy = gson.objectToNumberStrategy;
143147
this.numberToNumberStrategy = gson.numberToNumberStrategy;
148+
this.missingFieldValueStrategy = gson.missingFieldValueStrategy;
149+
this.unknownFieldStrategy = gson.unknownFieldStrategy;
144150
this.reflectionFilters.addAll(gson.reflectionFilters);
145151
}
146152

@@ -388,6 +394,36 @@ public GsonBuilder setObjectToNumberStrategy(ToNumberStrategy objectToNumberStra
388394
return this;
389395
}
390396

397+
/**
398+
* Configures Gson to apply a specific missing field value strategy during deserialization.
399+
* The strategy is used during reflection-based deserialization when the JSON data does
400+
* not contain a value for a field. A field with explicit JSON null is not considered missing.
401+
*
402+
* @param missingFieldValueStrategy strategy handling missing field values
403+
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
404+
* @see MissingFieldValueStrategy#DO_NOTHING The default missing field value strategy
405+
* @since $next-version$
406+
*/
407+
public GsonBuilder setMissingFieldValueStrategy(MissingFieldValueStrategy missingFieldValueStrategy) {
408+
this.missingFieldValueStrategy = Objects.requireNonNull(missingFieldValueStrategy);
409+
return this;
410+
}
411+
412+
/**
413+
* Configures Gson to apply a specific unknown field strategy during deserialization.
414+
* The strategy is used during reflection-based deserialization when an unknown field
415+
* is encountered in the JSON data.
416+
*
417+
* @param unknownFieldStrategy strategy handling unknown fields
418+
* @return a reference to this {@code GsonBuilder} object to fulfill the "Builder" pattern
419+
* @see UnknownFieldStrategy#IGNORE The default unknown field strategy
420+
* @since $next-version$
421+
*/
422+
public GsonBuilder setUnknownFieldStrategy(UnknownFieldStrategy unknownFieldStrategy) {
423+
this.unknownFieldStrategy = Objects.requireNonNull(unknownFieldStrategy);
424+
return this;
425+
}
426+
391427
/**
392428
* Configures Gson to apply a specific number strategy during deserialization of {@link Number}.
393429
*
@@ -782,7 +818,8 @@ public Gson create() {
782818
serializeSpecialFloatingPointValues, useJdkUnsafe, longSerializationPolicy,
783819
datePattern, dateStyle, timeStyle, new ArrayList<>(this.factories),
784820
new ArrayList<>(this.hierarchyFactories), factories,
785-
objectToNumberStrategy, numberToNumberStrategy, new ArrayList<>(reflectionFilters));
821+
objectToNumberStrategy, numberToNumberStrategy,
822+
missingFieldValueStrategy, unknownFieldStrategy, new ArrayList<>(reflectionFilters));
786823
}
787824

788825
private void addTypeAdaptersForDate(String datePattern, int dateStyle, int timeStyle,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package com.google.gson;
2+
3+
import com.google.gson.internal.reflect.ReflectionHelper;
4+
import com.google.gson.reflect.TypeToken;
5+
import java.lang.reflect.Field;
6+
7+
/**
8+
* A strategy defining how to handle missing field values during reflection-based deserialization.
9+
*
10+
* @see GsonBuilder#setMissingFieldValueStrategy(MissingFieldValueStrategy)
11+
* @since $next-version$
12+
*/
13+
public interface MissingFieldValueStrategy {
14+
/**
15+
* This strategy does nothing when a missing field is detected, it preserves the initial field
16+
* value, if any.
17+
*
18+
* <p>This is the default missing field value strategy.
19+
*/
20+
MissingFieldValueStrategy DO_NOTHING = new MissingFieldValueStrategy() {
21+
@Override
22+
public Object handleMissingField(TypeToken<?> declaringType, Object instance, Field field, TypeToken<?> resolvedFieldType) {
23+
// Preserve initial field value
24+
return null;
25+
}
26+
27+
@Override
28+
public String toString() {
29+
return "MissingFieldValueStrategy.DO_NOTHING";
30+
}
31+
};
32+
33+
/**
34+
* This strategy throws an exception when a missing field is detected.
35+
*/
36+
MissingFieldValueStrategy THROW_EXCEPTION = new MissingFieldValueStrategy() {
37+
@Override
38+
public Object handleMissingField(TypeToken<?> declaringType, Object instance, Field field, TypeToken<?> resolvedFieldType) {
39+
// TODO: Proper exception
40+
throw new RuntimeException("Missing value for field '" + ReflectionHelper.fieldToString(field) + "'");
41+
}
42+
43+
@Override
44+
public String toString() {
45+
return "MissingFieldValueStrategy.THROW_EXCEPTION";
46+
}
47+
};
48+
49+
/**
50+
* Called when a missing field value is detected. Implementations can either throw an exception or
51+
* return a default value.
52+
*
53+
* <p>Returning {@code null} will keep the initial field value, if any. For example when returning
54+
* {@code null} for the field {@code String f = "default"}, the field will still have the value
55+
* {@code "default"} afterwards (assuming the constructor of the class was called, see also
56+
* {@link GsonBuilder#disableJdkUnsafe()}). The type of the returned value has to match the
57+
* type of the field, no narrowing or widening numeric conversion is performed.
58+
*
59+
* <p>The {@code instance} represents an instance of the declaring type with the so far already
60+
* deserialized fields. It is intended to be used for looking up existing field values to derive
61+
* the missing field value from them. Manipulating {@code instance} in any way is not recommended.<br>
62+
* For Record classes (Java 16 feature) the {@code instance} is {@code null}.
63+
*
64+
* <p>{@code resolvedFieldType} is the type of the field with type variables being resolved, if
65+
* possible. For example if {@code class MyClass<T>} has a field {@code T myField} and
66+
* {@code MyClass<String>} is deserialized, then {@code resolvedFieldType} will be {@code String}.
67+
*
68+
* @param declaringType type declaring the field
69+
* @param instance instance of the declaring type, {@code null} for Record classes
70+
* @param field field whose value is missing
71+
* @param resolvedFieldType resolved type of the field
72+
* @return the field value, or {@code null}
73+
*/
74+
// TODO: Should this really expose `instance`? Only use case would be to derive value from other fields
75+
// but besides that user should not directly manipulate `instance` but return new value instead
76+
Object handleMissingField(TypeToken<?> declaringType, Object instance, Field field, TypeToken<?> resolvedFieldType);
77+
}

gson/src/main/java/com/google/gson/ReflectionAccessFilter.java

+16
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ enum FilterResult {
124124
? FilterResult.BLOCK_INACCESSIBLE
125125
: FilterResult.INDECISIVE;
126126
}
127+
128+
@Override public String toString() {
129+
return "ReflectionAccessFilter.BLOCK_INACCESSIBLE_JAVA";
130+
}
127131
};
128132

129133
/**
@@ -149,6 +153,10 @@ enum FilterResult {
149153
? FilterResult.BLOCK_ALL
150154
: FilterResult.INDECISIVE;
151155
}
156+
157+
@Override public String toString() {
158+
return "ReflectionAccessFilter.BLOCK_ALL_JAVA";
159+
}
152160
};
153161

154162
/**
@@ -173,6 +181,10 @@ enum FilterResult {
173181
? FilterResult.BLOCK_ALL
174182
: FilterResult.INDECISIVE;
175183
}
184+
185+
@Override public String toString() {
186+
return "ReflectionAccessFilter.BLOCK_ALL_ANDROID";
187+
}
176188
};
177189

178190
/**
@@ -198,6 +210,10 @@ enum FilterResult {
198210
? FilterResult.BLOCK_ALL
199211
: FilterResult.INDECISIVE;
200212
}
213+
214+
@Override public String toString() {
215+
return "ReflectionAccessFilter.BLOCK_ALL_PLATFORM";
216+
}
201217
};
202218

203219
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.google.gson;
2+
3+
import com.google.gson.reflect.TypeToken;
4+
import com.google.gson.stream.JsonReader;
5+
import java.io.IOException;
6+
7+
/**
8+
* A strategy defining how to handle unknown fields during reflection-based deserialization.
9+
*
10+
* @see GsonBuilder#setUnknownFieldStrategy(UnknownFieldStrategy)
11+
* @since $next-version$
12+
*/
13+
public interface UnknownFieldStrategy {
14+
/**
15+
* This strategy ignores the unknown field.
16+
*
17+
* <p>This is the default unknown field strategy.
18+
*/
19+
UnknownFieldStrategy IGNORE = new UnknownFieldStrategy() {
20+
@Override
21+
public void handleUnknownField(TypeToken<?> declaringType, Object instance, String fieldName,
22+
JsonReader jsonReader, Gson gson) throws IOException {
23+
jsonReader.skipValue();
24+
}
25+
26+
@Override
27+
public String toString() {
28+
return "UnknownFieldStrategy.IGNORE";
29+
}
30+
};
31+
32+
/**
33+
* This strategy throws an exception when an unknown field is encountered.
34+
*
35+
* <p><b>Note:</b> Be careful when using this strategy; while it might sound tempting
36+
* to strictly validate that the JSON data matches the expected format, this strategy
37+
* makes it difficult to add new fields to the JSON structure in a backward compatible way.
38+
* Usually it suffices to use only {@link MissingFieldValueStrategy#THROW_EXCEPTION} for
39+
* validation and to ignore unknown fields.
40+
*/
41+
UnknownFieldStrategy THROW_EXCEPTION = new UnknownFieldStrategy() {
42+
@Override
43+
public void handleUnknownField(TypeToken<?> declaringType, Object instance, String fieldName,
44+
JsonReader jsonReader, Gson gson) throws IOException {
45+
// TODO: Proper exception
46+
throw new RuntimeException("Unknown field '" + fieldName + "' for " + declaringType.getRawType() + " at path " + jsonReader.getPath());
47+
}
48+
49+
@Override
50+
public String toString() {
51+
return "UnknownFieldStrategy.THROW_EXCEPTION";
52+
}
53+
};
54+
55+
/**
56+
* Called when an unknown field is encountered. Implementations can throw an exception,
57+
* store the field value in {@code instance} or ignore the unknown field.
58+
*
59+
* <p>The {@code jsonReader} is positioned to read the value of the unknown field. If an
60+
* implementation of this method does not throw an exception it must consume the value, either
61+
* by reading it with methods like {@link JsonReader#nextString()} (possibly after peeking
62+
* at the value type first), or by skipping it with {@link JsonReader#skipValue()}.<br>
63+
* The {@code gson} object can be used to read from the {@code jsonReader}. It is the same
64+
* instance which was originally used to perform the deserialization.
65+
*
66+
* <p>The {@code instance} represents an instance of the declaring type with the so far already
67+
* deserialized fields. It can be used to store the value of the unknown field, for example
68+
* if it declares a {@code transient Map<String, Object>} field for all unknown values.<br>
69+
* For Record classes (Java 16 feature) the {@code instance} is {@code null}.
70+
*
71+
* @param declaringType type declaring the field
72+
* @param instance instance of the declaring type, {@code null} for Record classes
73+
* @param fieldName name of the unknown field
74+
* @param jsonReader reader to be used to read or skip the field value
75+
* @param gson {@code Gson} instance which can be used to read the field value from {@code jsonReader}
76+
* @throws IOException if reading or skipping the field value fails
77+
*/
78+
void handleUnknownField(TypeToken<?> declaringType, Object instance, String fieldName, JsonReader jsonReader, Gson gson) throws IOException;
79+
}

0 commit comments

Comments
 (0)