How do I override a Java map when converting a JSON to Java Object using GSON?

BlueChips23 :

I have a JSON string which looks like this:

{
 "status": "status",
 "date": "01/10/2019",
 "alerts": {
     "labels": {
         "field1": "value1",
         "field2": "value2",
         "field3": "value3",
         "field100": "value100"
     },
     "otherInfo" : "other stuff"
 },
 "description": "some description"
}

My corresponding Java classes look like the following:

public class Status {
    private String status;
    private String date;
    private Alerts alerts;
    private String description;
}

And

public class Alerts {
    private Map<String, String> labels;
    private String otherInfo;

    public Map<String, String> getLabels() {
        return labels();
    }
}

I'm parsing the given JSON into Java object using this:

Status status = gson.fromJson(statusJSONString, Status.class);

This also gives me Alerts object from Status class:

Alerts alerts = status.getAlerts();

Here is my problem:

Let's consider the labels:

I want to make keys in the label map the case-insensitive. So for example, if the provided key/value pair is "field1" : "value1", or "Field1" : "value1" or "fIeLD1":"value1", I want to be able to retrieve them by simply calling alerts.getLabels.get("field1").

Ideally, I want to set the keys to be lowercase when the labels map is originally created. I looked into Gson deserialization examples, but I'm not clear exactly how to approach this.

Michał Ziober :

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:

class LowercaseMapTypeAdapterFactory implements TypeAdapterFactory {

    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        TypeAdapter<String> stringAdapter = gson.getAdapter(TypeToken.get(String.class));

        return new TypeAdapter<T>() {
            @Override
            public void write(JsonWriter out, T value) { }

            @Override
            public T read(JsonReader in) throws IOException {
                JsonToken peek = in.peek();
                if (peek == JsonToken.NULL) {
                    in.nextNull();
                    return null;
                }

                Map<String, String> map = new HashMap<>();

                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) {
                        throw new JsonSyntaxException("duplicate key: " + key);
                    }
                }
                in.endObject();

                return (T) map;
            }
        };
    }
}

Now, we need to inform that our Map should be deserialised with our adapter:

class Alerts {

    @JsonAdapter(value = LowercaseMapTypeAdapterFactory.class)
    private Map<String, String> labels;

    private String 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;

public class GsonApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();
        Gson gson = new GsonBuilder().create();

        Status status = gson.fromJson(new FileReader(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:

class Labels {

    private final Map<String, String> map;

    public Labels(Map<String, String> map) {
        Objects.requireNonNull(map);
        this.map = new HashMap<>();
        map.forEach((k, v) -> this.map.put(k.toLowerCase(), v));
    }

    public String getValue(String label) {
        return this.map.get(label.toLowerCase());
    }

    // toString
}

Add new method to Alerts class:

public Map<String, String> toLabels() {
    return new Labels(labels);
}

Example usage:

status.getAlerts().toLabels()

Which gives you a very flexible and secure behaviour.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=144880&siteId=1