-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add strategies for unknown and missing fields #2358
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this should have a warning that users should be careful when implementing security critical validation with this, and should add extensive tests. Because if they forget to call |
||
* @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); | ||
Check notice Code scanning / CodeQL Useless parameter
The parameter 'instance' is never used.
Check notice Code scanning / CodeQL Useless parameter
The parameter 'resolvedFieldType' is never used.
Check notice Code scanning / CodeQL Useless parameter
The parameter 'declaringType' is never used.
Comment on lines
+74
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not completely sure whether it is really necessary to expose |
||
} |
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. | ||
* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe this should have a warning that users should be careful when implementing security critical validation with this, and should add extensive tests. Because if they forget to call |
||
* @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; | ||
Check notice Code scanning / CodeQL Useless parameter
The parameter 'instance' is never used.
Check notice Code scanning / CodeQL Useless parameter
The parameter 'gson' is never used.
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There might be use cases where a field excluded from deserialization should not be considered unknown, but should just be silently ignored.
With the current
UnknownFieldStrategy
API users might be able to determine themselves if a field is excluded, but not in a very reliable way since there are multiple ways in which a field can be excluded (e.g. modifiers, exclusion strategy,@Expose
) and Gson does not expose direct ways to test this.We could possibly add a
boolean excluded
parameter toUnknownFieldStrategy.handleUnknownField
whose value istrue
ifReflectiveTypeAdapterFactory
finds aBoundField
but it has!field.deserialized
. Though I am not sure if that is really worth it. What are your opinions?