How Do I Override A Java Map When Converting A Json To Java Object Using Gson?
Solution 1:
There isnt really much you can do here. Even if you extended HashMap, the problem is that when the JSON is de-serialized, it doesn't call native methods. What you COULD do is the following, but it is rather cumbersome:
import java.util.HashMap;
publicclassHashMapCaseInsensitiveextendsHashMap<String, String> {
privateboolean convertedToLower = false;
@OverridepublicStringput(String key, String value) {
if(!convertedToLower){
convertToLower();
}
returnsuper.put(key.toLowerCase(), value);
}
@OverridepublicStringget(Object key) {
if(!convertedToLower){
convertToLower();
}
returnsuper.get(key.toString().toLowerCase());
}
privatevoidconvertToLower(){
for(String key : this.keySet()){
String data = this.get(key);
this.remove(key);
this.put(key.toLowerCase(), data);
}
convertedToLower = true;
}
}
Solution 2:
You can write your own MapTypeAdapterFactory
which creates Map
always with lowered keys. Our adapter will be based on com.google.gson.internal.bind.MapTypeAdapterFactory
. We can not extend it because it is final
but our Map
is very simple so let's copy only important code:
classLowercaseMapTypeAdapterFactoryimplementsTypeAdapterFactory {
@Overridepublic <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
TypeAdapter<String> stringAdapter = gson.getAdapter(TypeToken.get(String.class));
returnnewTypeAdapter<T>() {
@Overridepublicvoidwrite(JsonWriter out, T value) { }
@Overridepublic T read(JsonReaderin) throws IOException {
JsonToken peek = in.peek();
if (peek == JsonToken.NULL) {
in.nextNull();
returnnull;
}
Map<String, String> map = newHashMap<>();
in.beginObject();
while (in.hasNext()) {
JsonReaderInternalAccess.INSTANCE.promoteNameToValue(in);
String key = stringAdapter.read(in).toLowerCase();
String value = stringAdapter.read(in);
String replaced = map.put(key, value);
if (replaced != null) {
thrownewJsonSyntaxException("duplicate key: " + key);
}
}
in.endObject();
return (T) map;
}
};
}
}
Now, we need to inform that our Map
should be deserialised with our adapter:
classAlerts {
@JsonAdapter(value = LowercaseMapTypeAdapterFactory.class)
privateMap<String, String> labels;
privateString otherInfo;
// getters, setters, toString
}
Assume that our JSON
payload
looks like below:
{"status":"status","date":"01/10/2019","alerts":{"labels":{"Field1":"value1","fIEld2":"value2","fielD3":"value3","FIELD100":"value100"},"otherInfo":"other stuff"},"description":"some description"}
Example usage:
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.internal.JsonReaderInternalAccess;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
publicclassGsonApp {
publicstaticvoidmain(String[] args)throws Exception {
FilejsonFile=newFile("./resource/test.json").getAbsoluteFile();
Gsongson=newGsonBuilder().create();
Statusstatus= gson.fromJson(newFileReader(jsonFile), Status.class);
System.out.println(status.getAlerts());
}
}
Above code prints:
Alerts{labels={field1=value1, field100=value100, field3=value3, field2=value2}, otherInfo='other stuff'}
This is really tricky solution and it should be used carefully. Do not use this adapter with much complex Map
-es. From other side, OOP
prefers much simple solutions. For example, create decorator
for a Map
like below:
classLabels {
private final Map<String, String> map;
publicLabels(Map<String, String> map) {
Objects.requireNonNull(map);
this.map = newHashMap<>();
map.forEach((k, v) -> this.map.put(k.toLowerCase(), v));
}
publicStringgetValue(String label) {
returnthis.map.get(label.toLowerCase());
}
// toString
}
Add new method to Alerts
class:
publicMap<String, String> toLabels() {
returnnewLabels(labels);
}
Example usage:
status.getAlerts().toLabels()
Which gives you a very flexible and secure behaviour.
Solution 3:
Though this is not a very generic solution, however, I think this will serve your purpose.
I would like to suggest you create an adapter for Gson which can convert the map values for you. The adapter might look like the following.
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import java.lang.reflect.Type;
finalclassGSONAdapterimplementsJsonDeserializer<String> {
privatestaticfinalGSONAdapterinstance=newGSONAdapter();
static GSONAdapter instance() {
return instance;
}
@Overridepublic String deserialize(JsonElement jsonElement, Type type,
JsonDeserializationContext jsonDeserializationContext)throws JsonParseException {
// Here I am taking the elements which are starting with field// and then returning the lowercase version// so that the labels map is created this wayif (jsonElement.getAsString().toLowerCase().startsWith("field"))
return jsonElement.getAsString().toLowerCase();
elsereturn jsonElement.getAsString();
}
}
Now just add the GsonBuilder
to your Gson using the adapter and then try to parse the JSON. You should get all the values in the lower case as you wanted for the labels
.
Please note that I am just taking the field
variables in my concern and hence this is not a generic solution which will work for every key. However, if your keys have any specific format, this can be easily applied.
Gsongson=newGsonBuilder()
.registerTypeAdapter(String.class, GSONAdapter.instance())
.create();
Statusstatus= gson.fromJson(statusJSONString, Status.class);
Alertsalerts= status.getAlerts();
Hope that solves your problem.
Post a Comment for "How Do I Override A Java Map When Converting A Json To Java Object Using Gson?"