Deserialize to interface implementations based on field name

1,748 views
Skip to first unread message

Brian

unread,
May 2, 2012, 3:47:59 PM5/2/12
to google-gson
Hi,

I created a custome Deserializer to deserialize into an object that
has two fields of the same interface type with different
implementations.

The class of the object to be deserialized is existing code and can
not be changed.

Is there any way to check the field name of the json string passed to
the deserializer to instantiate the object accordingly?

interface IAnimal {
}

class Tiger implements IAnimal {
}

class Lion implements IAnimal {
}

class Zoo {
IAnimal tiger;
IAnimal lion;
}

String json = {tiger: {}, lion: {}};

I'd like to deserialize the json string with tiger instantiated as an
object of Tiger based on the property name of "tiger" and the same for
lion.

Many thanks,

Brian

Brandon Mintern

unread,
May 2, 2012, 5:41:16 PM5/2/12
to googl...@googlegroups.com
You'll have to create a ZooDeserializer:


Zoo.java

import com.google.gson.*;
import java.lang.reflect.*;
import java.util.*;

public class Zoo {
private Animal tiger;
private Animal lion;

@Override
public String toString() {
return "Come see our triumphant tiger (" + tiger.toString()
+ ") and our laudable lion (" + lion.toString() + ") at the zoo!";
}

interface Animal {
}

static class Tiger implements Animal {
@Override
public String toString() {
return "with striking stripes";
}
}

static class Lion implements Animal {
@Override
public String toString() {
return "with a marvelous mane";
}
}

static class ZooDeserializer implements JsonDeserializer<Zoo> {
private static Map<String, Class<? extends Animal>> animals =
new HashMap();
static {
for (Class<? extends Animal> c: Arrays.asList(
// List each Animal implementer that you want to be
// able to deserialize here
Tiger.class,
Lion.class)) {
animals.put(c.getSimpleName().toLowerCase(), c);
}
}

/**
* Build a Zoo from json. This method will return null if:
* 1. json is not a JsonObject
* 2. a key in json is not a field in Zoo
* 3. a key in json does not correspond to a class in the animals map
* 4. a value in json cannot be properly deserialized into the named
* Animal class
*/
public Zoo deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
Zoo zoo = new Zoo();
try {
for (Map.Entry<String, JsonElement> kv:
json.getAsJsonObject().entrySet()) {
String name = kv.getKey();
Field f = Zoo.class.getDeclaredField(name);
f.setAccessible(true);
Animal a = context.deserialize(kv.getValue(),
animals.get(name));
f.set(zoo, a);
}
return zoo;
} catch (Exception e) {
return null;
}
}
}

public static void main(String[] args) {
String json = "{tiger:{},lion:{}}";
Gson gson = new GsonBuilder()
.registerTypeAdapter(Zoo.class, new ZooDeserializer())
.create();
Zoo zoo = gson.fromJson(json, Zoo.class);
System.out.println(zoo);
}
}


$ java -cp .:gson-2.1.jar Zoo

Come see our triumphant tiger (with striking stripes) and our laudable
lion (with a marvelous mane) at the zoo!


I wrote this deserializer in a somewhat generic way that would allow
to add a single instance of various different animals to your zoo:
each Animal type must be added to the ZooDeserializer static block.
With minor modifications, you could have a Zoo2 class with a list of
animals:


Zoo2.java

import com.google.gson.*;
import java.lang.reflect.*;
import java.util.*;

public class Zoo2 {
private final List<Animal> animals;

public Zoo2(List<Animal> animals) {
this.animals = animals;
}

@Override
public String toString() {
StringBuilder sb = new StringBuilder("Come see our many
animals at the zoo:");
for (Animal a: animals) {
sb.append('\n').append(a);
}
return sb.toString();
}

interface Animal {
}

static class Tiger implements Animal {
@Override
public String toString() {
return "a triumphant tiger with striking stripes";
}
}

static class Lion implements Animal {
@Override
public String toString() {
return "a laudable lion with a marvelous mane";
}
}

static class ZooDeserializer implements JsonDeserializer<Zoo2> {
private static Map<String, Class<? extends Animal>>
animalClasses = new HashMap();
static {
for (Class<? extends Animal> c: Arrays.asList(
// List each Animal implementer that you want to be
// able to deserialize here
Tiger.class,
Lion.class)) {
animalClasses.put(c.getSimpleName().toLowerCase(), c);
}
}

/**
* Build a Zoo2 from json. The json must be of the form:
*
* { animalType: {},
* anotherType: [{}, {}],
* ... }
*
* The animalType must correspond to the lowercase version of a class
* registered in the static initialization block of this deserializer.
* The value corresponding to that key must be a single JsonObject
* representing an instance of the named class, or a list of such
* JsonObjects.
*
* @throws JsonParseException if the json does not represent a proper
* zoo object.
*/
public Zoo2 deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
List<Animal> animals = new ArrayList();
for (Map.Entry<String, JsonElement> kv:
json.getAsJsonObject().entrySet()) {
Class animalClass = animalClasses.get(kv.getKey());
JsonElement obj = kv.getValue();
if (obj.isJsonArray()) {
for (JsonElement j: obj.getAsJsonArray()) {
animals.add((Animal) context.deserialize(j,
animalClass));
}
} else {
animals.add((Animal) context.deserialize(obj, animalClass));
}
}
return new Zoo2(animals);
}
}

public static void main(String[] args) {
String json = "{tiger:{},lion:[{},{}]}";
Gson gson = new GsonBuilder()
.registerTypeAdapter(Zoo2.class, new ZooDeserializer())
.create();
Zoo2 zoo = gson.fromJson(json, Zoo2.class);
System.out.println(zoo);
}
}


$ java -cp .:gson-2.1.jar Zoo2

Come see our many animals at the zoo:
a triumphant tiger with striking stripes
a laudable lion with a marvelous mane
a laudable lion with a marvelous mane
> --
> You received this message because you are subscribed to the Google Groups "google-gson" group.
> To post to this group, send email to googl...@googlegroups.com.
> To unsubscribe from this group, send email to google-gson...@googlegroups.com.
> For more options, visit this group at http://groups.google.com/group/google-gson?hl=en.
>

Brian

unread,
May 3, 2012, 3:30:53 AM5/3/12
to google-gson
Hi Brandon,

Thanks a lot for the sample code.

But I was aware of this approach which could require too much coding
if the "Zoo" class had many other fields of complex objects that would
have to be specifically coded for deserialization.

Is it possible to create deserializer for Animal field itself but
still have access to the field name of the json being deserialized (or
the position of it in the hierarchy of the whole json object)?

Thanks,

Brian

Brandon Mintern

unread,
May 3, 2012, 2:24:15 PM5/3/12
to googl...@googlegroups.com
Hi Brian,

On Thu, May 3, 2012 at 12:30 AM, Brian <bitte...@gmail.com> wrote:
> Is it possible to create deserializer for Animal field itself but
> still have access to the field name of the json being deserialized (or
> the position of it in the hierarchy of the whole json object)?

I was afraid that might be the case. I'm not aware of a
straightforward way to do this. I came up with the following hack to
get you started, but it's somewhat brittle because it relies on Gson's
implementation details (visiting field names in the same order that
object values are visited). I also haven't tested it thoroughly or
with more complicated objects. I hope it works for you.

$ cat ZooByFieldName.java
import com.google.gson.*;
import java.lang.reflect.*;
import java.util.*;

public class ZooByFieldName {
static class Zoo {
Animal tiger;
Animal lion;
@Override
public String toString() {
return "zoo with a tiger (" + tiger.toString()
+ ") and a lion (" + lion.toString() + ")";
}
}

interface Animal {}

static class Tiger implements Animal {
@Override
public String toString() {
return "triumphant tiger";
}
}

static class Lion implements Animal {
@Override
public String toString() {
return "laudable lion";
}
}

abstract static class FieldAwareDeserializer<T> implements
JsonDeserializer<T> {
private final LinkedList<String> keys = new LinkedList<String>();

abstract protected Class<T> getDeserializedClass();
abstract protected T deserialize(JsonElement json, String
field, Type typeOfT, JsonDeserializationContext context);

public T deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
return deserialize(json, keys.remove(0), typeOfT, context);
}

public void encounteredField(Field f) {
if (getDeserializedClass().isAssignableFrom(f.getType())) {
keys.add(f.getName());
}
}
}

static class AnimalDeserializer extends FieldAwareDeserializer<Animal> {
private final String classLocation;

public AnimalDeserializer() {
String c = AnimalDeserializer.class.getName();
int splitAt = Math.max(c.lastIndexOf('.'), c.lastIndexOf('$'));
classLocation = c.substring(0, splitAt + 1);
}

@Override
protected Class<Animal> getDeserializedClass() {
return Animal.class;
}

@Override
protected Animal deserialize(JsonElement json, String field,
Type typeOfT, JsonDeserializationContext context) throws
JsonParseException {
String animal = field.substring(0, 1).toUpperCase() +
field.substring(1);
try {
Class animalClass = Class.forName(classLocation + animal);
return (Animal) animalClass.newInstance();
} catch (ClassNotFoundException e) {
throw new JsonParseException(e);
} catch (InstantiationException e) {
throw new JsonParseException(e);
} catch (IllegalAccessException e) {
throw new JsonParseException(e);
}
}
}

static class FieldGrabber implements FieldNamingStrategy {
private final List<FieldAwareDeserializer> deserializers = new
ArrayList();

@Override
public String translateName(Field f) {
for (FieldAwareDeserializer d: deserializers) {
d.encounteredField(f);
}
return f.getName();
}

public void registerDeserializer(FieldAwareDeserializer d) {
deserializers.add(d);
}
}

public static void main(String[] args) {
String json = "{tiger:{},lion:{}}";
AnimalDeserializer deserializer = new AnimalDeserializer();
FieldGrabber grabber = new FieldGrabber();
grabber.registerDeserializer(deserializer);
Gson gson = new GsonBuilder()
.setFieldNamingStrategy(grabber)
.registerTypeAdapter(Animal.class, deserializer)
.create();
Zoo zoo = gson.fromJson(json, Zoo.class);
System.out.println(zoo);
}
}

$ java -cp .:gson-2.1.jar ZooByFieldName
zoo with a tiger (triumphant tiger) and a lion (laudable lion)

Brandon Mintern

unread,
May 3, 2012, 2:26:49 PM5/3/12
to googl...@googlegroups.com
One more thing... that solution is not parallel-safe (the Gson
instance can only be used one-at-a-time). But perhaps you can build
something that is thread-safe based on my code.

Brian

unread,
May 7, 2012, 4:10:45 PM5/7/12
to google-gson
Thanks Brandon. That was very helpful!

Brian

On May 3, 7:26 pm, Brandon Mintern <mint...@easyesi.com> wrote:
> One more thing... that solution is not parallel-safe (the Gson
> instance can only be used one-at-a-time). But perhaps you can build
> something that is thread-safe based on my code.
>
>
>
>
>
>
>
> On Thu, May 3, 2012 at 11:24 AM, Brandon Mintern <mint...@easyesi.com> wrote:
> > Hi Brian,
>
Reply all
Reply to author
Forward
0 new messages