Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Write two JSON entries from one Java field

Tags:

java

gson

The Problem:

I have a bunch of DTOs with a structure like this:

public class Foobar {
    private String name;
    private Timestamp time1;
    private Timestamp time2;
    private int value;
}

I need to serialize the Timestamps to two separate values (one time call .toString() and one time formatted according to ISO standard) in order to be backward compatible with old API and also support a decent time format from now on.

So the JSON output for Foobar should look like this:

{
    "name":"<some name>",
    "time1":"<some non standard time>",
    "iso_time1":"<ISO formatted time>",
    "time2":"<some non standard time>",
    "iso_time2":"<ISO formatted time>",
    "value":<some number>
}

I am restricted to Gson due to existing code.

Question:

Is it possible to do this in a generic way, that will work for all my DTOs without changing the DTOs?

I want to avoid having to write a TypeAdapter/Serializer/new DTO for each of my existing DTOs.

What I tried

TypeAdapter

I already tried to do it via a TypeAdapter and TypeAdapterFactory but I need the field name of the class in order to distinguish the two timestamps.

write(...) method of the TypeAdapter illustrating the issue I had with it (T extends Timestamp):

@Override
public void write(final JsonWriter out, final T value) throws IOException {
    out.value(value.toString());
    out.name(TIMESTAMP_ISO_PREFIX + fieldName).value(toISOFormat(value));
}

The Problem here is, that I did not find any way to get to the field name. I tried to get it with a TypeAdapterFactory but the factory does not know the field name either.

JsonSerializer

I also tried to do it via JsonSerializer, but it is not possible to return two JSON elements and returning a JsonObject would break existing API.

like image 517
user9113868 Avatar asked Dec 18 '17 14:12

user9113868


People also ask

How do I add two JSON objects in Java?

JSONObject to merge two JSON objects in Java. We can merge two JSON objects using the putAll() method (inherited from interface java. util.

What is JsonProperty annotation Java?

The @JsonProperty annotation is used to map property names with JSON keys during serialization and deserialization. By default, if you try to serialize a POJO, the generated JSON will have keys mapped to the fields of the POJO.

What is the use of JsonAlias?

The @JsonAlias annotation can define one or more alternate names for the attributes accepted during the deserialization, setting the JSON data to a Java object. But when serializing, i.e. getting JSON from a Java object, only the actual logical property name is used instead of the alias.


1 Answers

Approach 1: Use a JsonSerialiser

You can create a JsonSerialiser for your objects (i.e. one level higher than the Timestamp) and use it to append extra fields as needed:

/**
 * Appends extra fields containing ISO formatted times for all Timestamp properties of an Object.
 */
class TimestampSerializer implements JsonSerializer<Object> {
    private Gson gson = new GsonBuilder().create();

    @Override
    public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContext context) {
        JsonElement tree = gson.toJsonTree(src);
        if (tree instanceof JsonObject) {
            appendIsoTimestamps(src, (JsonObject) tree);
        }
        return tree;
    }

    private JsonObject appendIsoTimestamps(Object src, JsonObject object) {
        try {
            PropertyDescriptor[] descriptors = Introspector.getBeanInfo(src.getClass()).getPropertyDescriptors();
            for (PropertyDescriptor descriptor : descriptors) {
                if (descriptor.getPropertyType().equals(Timestamp.class)) {
                    Timestamp ts = (Timestamp) descriptor.getReadMethod().invoke(src);
                    object.addProperty("iso_" + descriptor.getName(), ts.toInstant().toString());
                }
            }
            return object;
        } catch (IllegalAccessException | InvocationTargetException | IntrospectionException e) {
            throw new JsonIOException(e);
        }
    }

Example usage:

public class GsonSerialiserTest {
    public static void main(String[] args) {
        GsonBuilder builder = new GsonBuilder();
        builder.registerTypeAdapter(Foobar.class, new TimestampSerializer());
        Gson gson = builder.create();
        Foobar baz = new Foobar("baz", 1, new Timestamp(System.currentTimeMillis()));
        System.out.println(gson.toJson(baz));
    }
}

Some notes:

  • This example uses the java bean introspector to find Timestamp properties. It relies on the presence of getter methods. If you don't have getters you'll have to use some other method to read your timestamp properties.
  • The serialiser delegates to another gson builder (it can't call the one in the JsonSerializationContext or it would end up calling itself recursively). If your existing serialisation relies on other instrumentation in the builder you'll have to wire up a separate builder and pass it to the serialiser.

If you want to do this for all objects you're serialising register the adapter for the entire Object hierarchy:

builder.registerTypeHierarchyAdapter(Object.class, typeAdapter);

If you just want to modify a subset of DTOs you can register them dynamically. The Reflections library makes this easy:

TimestampSerializer typeAdapter = new TimestampSerializer();

Reflections reflections = new Reflections(new ConfigurationBuilder()
    .setScanners(new SubTypesScanner(false))
    .setUrls(ClasspathHelper.forClassLoader(ClasspathHelper.contextClassLoader()))
    .filterInputsBy(new FilterBuilder().includePackage("com.package.dto", "com.package.other")));

Set<Class<?>> classes = reflections.getSubTypesOf(Object.class);

for (Class<?> type : classes) {
    builder.registerTypeAdapter(type, typeAdapter);
}

The example above registers everything in the named packages. If your DTOs conform to a naming pattern or implement a common interface/have a common annotation you could further restrict what gets registered.

Approach 2: Register a TypeAdapterFactory

TypeAdapters work at the reader/writer level and require a little more work to implement, but they give you more control.

Registering a TypeAdapterFactory with the builder will allow you to control which types to edit. This example applies the adapter to all types:

public static void main(String[] args) {
    GsonBuilder builder = new GsonBuilder();

    builder.registerTypeAdapterFactory(new TypeAdapterFactory() {
        public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
            // Return null here if you don't want to handle the type.
            // This example returns an adapter for every type.
            return new TimestampAdapter<>(type);
        }
    });

    Gson gson = builder.create();
    Foobar baz = new Foobar("baz", 1);
    String json = gson.toJson(baz);
    System.out.println(json);
    System.out.println(gson.fromJson(json, Foobar.class));
}

And the adapter...

class TimestampAdapter<T> extends TypeAdapter<T> {
    private TypeToken<T> type;
    private Gson gson = new GsonBuilder().create();

    public TimestampAdapter(TypeToken<T> type) {
        this.type = type;
    }

    @Override
    public void write(JsonWriter out, T value) throws IOException {
        JsonObject object = appendIsoTimestamps(value, (JsonObject) gson.toJsonTree(value));
        TypeAdapters.JSON_ELEMENT.write(out, object);
    }

    private JsonObject appendIsoTimestamps(T src, JsonObject tree) {
        try {
            PropertyDescriptor[] descriptors = Introspector.getBeanInfo(src.getClass()).getPropertyDescriptors();
            for (PropertyDescriptor descriptor : descriptors) {
                if (descriptor.getPropertyType().equals(Timestamp.class)) {
                    Timestamp ts = (Timestamp) descriptor.getReadMethod().invoke(src);
                    tree.addProperty("iso_" + descriptor.getName(), ts.toInstant().toString());
                }
            }
            return tree;
        } catch (IllegalAccessException | InvocationTargetException | IntrospectionException e) {
            throw new JsonIOException(e);
        }
    }

    @Override
    public T read(JsonReader in) {
        return gson.fromJson(in, type.getType());
    }
}
like image 186
teppic Avatar answered Sep 26 '22 00:09

teppic