I’m developing a Java library that interacts with a REST service. The REST responses are JSON object and the library deserialize it using the Google Gson library. Interacting with the service I had a special case: one field of an object was serialized as JSON in two different ways depending on request url. In one case the field was serialized as JSON object, in the other case as JSON string.
In order to explain the problem and the solution I’ve simplified it.
Let’s have a Person object:
public class Person {
protected String name;
protected String surname;
protected int age;
protected Address address;
//... Constructor, getters and setters
}
and an Address object:
public class Address {
protected String street;
protected int number;
protected String city;
//... constructor, getters and setters ...
}
I would expect these objects serialized in JSON like this:
{
"name": "John",
"surname": "Smith",
"age": 32,
"address": {
"street": "Amphitheatre Parkway",
"number": 1600,
"city": "Mountain View"
}
}
And I would deserialize it using the Gson library with this code:
public class Deserialize {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
String json = Resources.toString(Resources.getResource("default.json"), Charsets.UTF_8);
Gson gson = new Gson();
Person person = gson.fromJson(json, Person.class);
System.out.println(person);
}
}
But as explained before the REST service that I’m using is not always returning a standard serialization. The service sometime returns the Person object serialized in this way:
{
"name": "John",
"surname": "Smith",
"age": 32,
"address": "/location/id/01"
}
Where the address field is not a JSON object but a JSON string.
The solution that I’ve found is to customize the Address type serialization. The custom serialization will deserialize the JSON object as normal if there is a JSON object, otherwise it will skip it and set the value to null in case of JSON string.
One way to get to this solution using the Gson library is to write a TypeAdapter for the Address type. In the custom adapter you will check the passed JSON element type. If it is JSON string the adapter then returns a null value. If the type is a JSON object the adapter deserialize it. In order to deserialize it you should to write the code necessary to handle the object deserialization. That means writing annoying code when Gson already knows how to deserialize a Java object.
Let’s avoid the boiler code and try to use the Gson deserialization.
First we need a custom TypeAdapterFactory:
public class AddressAdapterFactory implements TypeAdapterFactory {
@Override
@SuppressWarnings("unchecked")
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
if (type.getRawType()!=Address.class) return null;
TypeAdapter<Address> defaultAdapter = (TypeAdapter<Address>) gson.getDelegateAdapter(this, type);
return (TypeAdapter<T>) new AddressAdapter(defaultAdapter);
}
}
A TypeAdapterFactory provides TypeAdapter for a certain type T. In our case we need a TypeAdapterFactory for the Address object type.
Our custom adapter checks if the type is the one that we want to manage: Address.class.Then it retrieves an alternative adapter for the type Address.class.
The getDelegateAdapter method from the GSon class tries to find a TypeAdapterFactory that can provide a TypeAdapter for the specified type. To avoid to get back our custom adapter we pass it as first parameter, this tell the method to skip it.
The only adapter will be the one provided by the Gson library, the default one that use reflection in order to serialize/deserialize a Java object.
After we found the default adapter we return our customized TypeAdapter:
public class AddressAdapter extends TypeAdapter<Address> {
protected TypeAdapter<Address> defaultAdapter;
/**
* @param defaultAdapter
*/
public AddressAdapter(TypeAdapter<Address> defaultAdapter) {
this.defaultAdapter = defaultAdapter;
}
/**
* {@inheritDoc}
*/
@Override
public void write(JsonWriter out, Address value) throws IOException {
defaultAdapter.write(out, value);
}
/**
* {@inheritDoc}
*/
@Override
public Address read(JsonReader in) throws IOException {
if (in.peek()==JsonToken.STRING) {
in.skipValue();
return null;
}
return defaultAdapter.read(in);
}
}
The AddressAdapter, in the deserialization case will check if the incoming JSON element is a string or not. If the element is a string the adapter will skip it and return a null value. If the element is an object the adapter will use the defaultAdapter in order to deserialize it.
You can find an Eclipse project containing the sample code here.