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.
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.
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.
JSONObject to merge two JSON objects in Java. We can merge two JSON objects using the putAll() method (inherited from interface java. util.
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.
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.
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:
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.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.
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());
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With