简体   繁体   中英

Gson: How to change specific fields key from serialization

I am using Gson for serialization and I'm struggling with changing field names dynamically. Here is my class:

public class Response<T> 
{

    private String status;
    private String message;
    private T data;

    public Response(T data) 
    {
        this.setData(data);
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }     

}

I have to change the field names dynamically based on the resource. Is there any way to change this?

Using maps may be not the best choice since your Response class may have special Gson annotations that will be ignored once your response objects are converted to maps.

Suppose the following simple response class:

final class Response<T> {

    @Expose(serialize = true)
    final String status = "STATUS";

    @Expose(serialize = true)
    final String message = "MESSAGE";

    @Expose(serialize = true)
    final T data;

    @Expose(serialize = false, deserialize = false)
    final String whatever = "WHATEVER";

    Response(final T data) {
        this.data = data;
    }

}

This response does not use another Gson annotations for simplicity. Ad hoc use of a dynamic field renaming:

final Gson gson = new GsonBuilder()
        .excludeFieldsWithoutExposeAnnotation()
        // ... any Gson configuration here ...
        .create();
final Response<List<String>> response = new Response<>(ImmutableList.of("foo", "bar"));
final JsonElement jsonTree = gson.toJsonTree(response, stringListResponseTypeToken.getType());
final JsonObject responseJson = jsonTree.getAsJsonObject();
final JsonElement dataPropertyJson = responseJson.get("data");
responseJson.remove("data");
responseJson.add(response.getClass().getSimpleName(), dataPropertyJson);
gson.toJson(responseJson, System.out);

Note that the main trick here is creating an intermediate JSON tree and substituting the dynamic property name. Unfortunately, this solution requires an intermediate JSON tree. Another, more "Gson-ish" solution is creating a special type adapter in order not to re-map the response objects everytime it's necessary.

final Gson gson = new GsonBuilder()
        .excludeFieldsWithoutExposeAnnotation()
        // ... any Gson configuration here ...
        .registerTypeAdapterFactory(getDynamicPropertyResponseTypeAdapterFactory())
        .create();
final Response<List<String>> response = new Response<>(ImmutableList.of("foo", "bar"));
gson.toJson(response, stringListResponseTypeToken.getType(), System.out);

Where the type adapter factory and type adapters are implemented as follows:

final class DynamicPropertyResponseTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeAdapterFactory dynamicPropertyResponseTypeAdapterFactory = new DynamicPropertyResponseTypeAdapterFactory();

    private DynamicPropertyResponseTypeAdapterFactory() {
    }

    static TypeAdapterFactory getDynamicPropertyResponseTypeAdapterFactory() {
        return dynamicPropertyResponseTypeAdapterFactory;
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        if ( Response.class.isAssignableFrom(typeToken.getRawType()) ) {
            @SuppressWarnings("unchecked")
            final TypeAdapter<Response<Object>> delegateTypeAdapter = (TypeAdapter<Response<Object>>) gson.getDelegateAdapter(this, typeToken);
            @SuppressWarnings("unchecked")
            final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) getDynamicPropertyResponseJsonTypeAdapter(delegateTypeAdapter, gson);
            return castTypeAdapter;
        }
        return null;
    }

}

Note that this type adapter factory picks the downstream type adapter to avoid infinite recursion if the handled class is Response , and otherwise null is returned to let Gson use its own (de)serialization strategies.

final class DynamicPropertyResponseJsonTypeAdapter<T>
        extends TypeAdapter<Response<T>> {

    private final TypeAdapter<Response<T>> delegateTypeAdapter;
    private final Gson gson;

    private DynamicPropertyResponseJsonTypeAdapter(final TypeAdapter<Response<T>> delegateTypeAdapter, final Gson gson) {
        this.delegateTypeAdapter = delegateTypeAdapter;
        this.gson = gson;
    }

    static <T> TypeAdapter<Response<T>> getDynamicPropertyResponseJsonTypeAdapter(final TypeAdapter<Response<T>> delegateTypeAdapter, final Gson gson) {
        return new DynamicPropertyResponseJsonTypeAdapter<>(delegateTypeAdapter, gson);
    }

    @Override
    @SuppressWarnings("resource")
    public void write(final JsonWriter out, final Response<T> response)
            throws IOException {
        if ( response == null ) {
            out.nullValue();
            return;
        }
        final JsonElement jsonTree = delegateTypeAdapter.toJsonTree(response);
        final JsonObject responseJson = jsonTree.getAsJsonObject();
        final JsonElement dataPropertyJson = responseJson.get("data");
        responseJson.remove("data");
        responseJson.add(response.getClass().getSimpleName(), dataPropertyJson);
        gson.toJson(responseJson, out);
    }

    @Override
    public Response<T> read(final JsonReader in) {
        throw new UnsupportedOperationException();
    }

}

The same not very cheap trick is used above, but now it works as a part of the Gson instance. For both cases the output is as follows:

{"status":"STATUS","message":"MESSAGE","Response":["foo","bar"]}

Another options you might want to take into account are:

  • Making the Response class abstract and letting the child classes to define their own data field names via @SerializedName if the names are supposed to be hard-coded.
  • Creating your implementation ReflectiveTypeAdapterFactory (see the Gson source code) and making the fields names dynamic without creating an intemediate JSON trees.

If you need to change the field's name than it means you don't need type safety, so the following will do:

Map<String, Object> response = new LinkedHashMap<>();
response.put("message", message);
response.put("status", status);
response.put(dynamicFieldName, dynamicFieldValue);
String json = gson.toJson(response);

You can still wrap a convenience library on top of this low-level code to account for common use cases.

Why not use HashMap for such cases?

private HashMap<String, String> data;

public HashMap<String, String> getData() {
    return this.data;
}

public void setDataValue(String key, String value) {
    data.put(key, value);
}

Note that the data field can be a HashMap of <String, Object> or <Long, Object> too, to save sub-objects, thus accepting all kind of Gson supported structures.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM