Navneeth
Navneeth

Reputation: 423

How to have a single GSON custom serializer to apply to all subclasses?

I'm using GSON to apply a universal serializer to all subclasses of an abstract Base class. However, GSON will not call my serializer when given actual subclasses of the Base class unless explicitly told to use Base.class as a cast. Here's a simple instance of what I'm talking about.

public interface Base<T>{
  String getName();
  public List<Object> getChildren();
}

public class Derived1 implements Base<Integer>{
  private Integer x = 5; 
  String getName(){ 
    return "Name: " + x;
  }
  List<Object> getChildren(){
    return Lists.newArrayList(new Derived2(), "Some string");
  }
}

public class Derived2 implements Base<Double>{
  private Double x = 6.3;
  String getName(){
    return "Name: " + x;
  }
  List<Object> getChildren(){
    return new List<>();
  }
}

I'm creating a serializer as follows:

JsonSerializer customAdapter = new JsonSerializer<Base>(){
  @Override
  JsonElement serialize(Base base, Type sourceType, JsonSerializationContext context){
    JsonObject jsonObject = new JsonObject();
    jsonObject.addProperty("name", base.getName());
    JsonArray jsonArray = new JsonArray();
    for (Object child : base.getChildren()){
      jsonArray.add(context.serialize(child));
    }
    if (jsonArray.size() != 0){
      jsonObject.add("children", jsonArray);
    }
  }
};

Gson customSerializer = new GsonBuilder()
  .registerTypeAdapter(Base.class, customAdapter)
  .create();

However, applying my custom serializer to a List of subclasses does not have the desired effect.

customSerializer.toJson(Lists.newArrayList(new Derived1(), new Derived2()));

This applies the default GSON serialization to my subclasses. Is there any easy way to get my custom serializer to use my custom adapter on all subclasses of the parent class? I suspect that one solution is to use reflection to iterate over all subclasses of Base and register the custom adapter, but I'd like to avoid something like that if possible.

Note: I don't care about deserialization right now.

Upvotes: 2

Views: 2637

Answers (1)

pirho
pirho

Reputation: 12285

Maybe you should not use JsonSerializer. Namely, this is possible if you use TypeAdapter doing the same magic by registering TypeAdapterFactory that tells Gson how to serialize any class.

See below TypeAdapterFactory and TypeAdapter in it:

public class CustomAdapterFactory implements TypeAdapterFactory {

    @SuppressWarnings("unchecked")
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
        // If the class that type token represents is a subclass of Base
        // then return your special adapter 
        if(Base.class.isAssignableFrom(typeToken.getRawType())) {
            return (TypeAdapter<T>) customTypeAdapter;          
        }
        return null;
    }

    private TypeAdapter<Base<?>> customTypeAdapter = new TypeAdapter<Base<?>>() {
        @Override
        public void write(JsonWriter out, Base<?> value) throws IOException {
            out.beginObject();
            out.value(value.getName());
            out.endObject();            
        }

        @Override
        public Base<?> read(JsonReader in) throws IOException {
            // Deserializing to subclasses not interesting yet.
            // Actually it is impossible if the JSON does not contain 
            // information about the subclass to which to deserialize
            return null;
        }

    };
}

If you do something like this:

@Slf4j
public class SubClassTest {
    @Test
    public void testIt() {
        Gson gson = new GsonBuilder()
                .setPrettyPrinting()
                .registerTypeAdapterFactory(new CustomAdapterFactory())
                .create();
        log.info("\n{}", gson.toJson(new Derived1()));
        log.info("\n{}", gson.toJson(new Derived2()));
    }
}

the output will be like this:

2018-10-12 23:13:17.037 INFO org.example.gson.subclass.SubClassTest:19 - { "name": "Name: 5" } 2018-10-12 23:13:17.043 INFO org.example.gson.subclass.SubClassTest:20 - { "name": "Name: 6.3" }

If it is not exactly what you want just fix the write(..) method in the customTypeAdapter.

Upvotes: 3

Related Questions