I have a collection of generated java beans where each bean defines one or more fields, plus it subclasses HashMap<String, T>
where T
is a parameterised type. The generated fields model explicitly-defined schema properties within a JSON schema
, and the subclassing of HashMap
is done to support additional "arbitrary" properties of a specific type (specified by the JSON schema's "additionalProperties" field, for those that are familiar with JSON Schema).
Here is an example of a generated bean:
public class MyModel extends HashMap<String, Foo> {
private String prop1;
private Long prop2;
public String getProp1() {
return prop1;
}
public void setProp1(String value) {
this.prop1 = value;
}
public Long getProp2() {
return prop2;
}
public void setProp2(Long prop2) {
this.prop2 = prop2;
}
}
In this example, the user can set prop1
or prop2
as normal bean properties and can also set arbitrary properties of type Foo
via the Map
's put()
method, where Foo
is just some other user-defined type.
The problem is that, by default, Gson
serialises instances of these generated beans such that only the Map
entries are included in the resulting JSON
string, and the explicitly-defined fields are ignored.
Here is a code snippet showing how I use Gson
to serialise an object:
private String serialize(Object obj) {
return new Gson().toJson(obj);
}
From debugging the serialisation path, I can see that Gson
is selecting its internal MapTypeAdapterFactory
to perform the serialization which, makes sense since only the Map entries end up in the JSON string.
Separately, if I serialize a bean that does NOT subclass HashMap, then Gson
selects its internal ReflectiveTypeAdapterFactory
instead.
I think what I need is to implement my own custom type adapter that essentially combines the functionality of the Reflective
and Map
type adapter factories.
Does this sound like a good plan? Has anyone else done something similar to this and could perhaps provide an example to get me started? This will be my first foray into Gson
custom type adapters.
We know that by default Gson
treats these objects like Map
so we can use it to serialise all key-value
pairs and manually serialise rest of them using reflection.
Simple serialiser implementation could look like below:
class MixedJsonSerializer implements JsonSerializer<Object> {
@Override
public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContext context) {
JsonObject json = serialiseAsMap(src, context);
serialiseAsPojo(src, context, json);
return json;
}
private JsonObject serialiseAsMap(Object src, JsonSerializationContext context) {
return (JsonObject) context.serialize(src, Map.class);
}
private void serialiseAsPojo(Object src, JsonSerializationContext context, JsonObject mapElement) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(src.getClass());
for (Method method : methods) {
if (shouldSerialise(method)) {
final Object result = ReflectionUtils.invokeMethod(method, src);
final String fieldName = getFieldName(method);
mapElement.add(fieldName, context.serialize(result));
}
}
}
private boolean shouldSerialise(Method method) {
final String name = method.getName();
return method.getParameterCount() == 0 &&
ReflectionUtils.USER_DECLARED_METHODS.matches(method) &&
!IGNORED_METHODS.contains(name) &&
(name.startsWith("is") || name.startsWith("get"));
}
private static final List<String> IGNORED_METHODS = Arrays.asList("isEmpty", "length"); //etc
private String getFieldName(Method method) {
final String field = method.getName().replaceAll("^(is|get)", "");
return StringUtils.uncapitalize(field);
}
}
The most complex part is find all POJO
getters and invoke them on given object. I used reflection API
from Spring
library just for example. Below you can find example how to use it (I assume that all POJO
classes extends HashMap
):
import com.model.Foo;
import com.model.Pojo;
import com.model.Pojo1;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class GsonApp {
public static void main(String[] args) {
System.out.println("Pojo + Map: ");
Pojo pojo = new Pojo();
pojo.put("character1", new Foo("Morty", 15));
pojo.put("character2", new Foo("Rick", 60));
System.out.println(serialize(pojo));
System.out.println();
System.out.println("Map only: ");
Pojo1 pojo1 = new Pojo1();
pojo1.put("int1", 1);
pojo1.put("int2", 22);
System.out.println(serialize(pojo1));
System.out.println();
System.out.println("Pojo only:");
System.out.println(serialize(new Pojo()));
System.out.println();
}
private static final Gson gson = createGson();
private static Gson createGson() {
MixedJsonSerializer adapter = new MixedJsonSerializer();
return new GsonBuilder()
.setPrettyPrinting()
// in case you have many classes you need to use reflection
// to register adapter for each needed class.
.registerTypeAdapter(Pojo.class, adapter)
.registerTypeAdapter(Pojo1.class, adapter)
.create();
}
private static String serialize(Object obj) {
return gson.toJson(obj);
}
}
Above code prints:
Pojo + Map:
{
"character2": {
"name": "Rick",
"age": 60
},
"character1": {
"name": "Morty",
"age": 15
},
"prop1": "Value1",
"ten": 10,
"foo": {
"name": "Test",
"age": 123
}
}
Map only:
{
"int2": 22,
"int1": 1
}
Pojo only:
{
"prop1": "Value1",
"ten": 10,
"foo": {
"name": "Test",
"age": 123
}
}
If you have to many classes to register them manually you can use reflection to scan given packages and register serialiser for them. See: Can you find all classes in a package using reflection?