Slimsim3
Slimsim3

Reputation: 135

Gson: How do I deserialize interface fields having class names?

I am trying to deserialize a List of Research objects but I can't make it work. I know that I need a custom adapter to deserialize my object since I am using an interface in my Research class but I am unsure on how to implement it.

I currently have a serializer which seems to work and saves the class type for desirialization. I've been using bits of code from this SO post to make the serializer: Gson deserialize interface to its Class implementation

This is the JSON I'm working with:

[
   {
      "bought":false,
      "cost":-20,
      "effect":{
         "amount":1,
         "className":"com.example.slarocque.cellclicker.Research.ResearchEffects.ClickAmountEffectStatic"
      },
      "name":"Better Flasks"
   },
   {
      "bought":false,
      "cost":-100,
      "effect":{
         "className":"com.example.slarocque.cellclicker.Research.ResearchEffects.ClickAmountEffectPercent",
         "percent":120
      },
      "name":"Buy a new Heater"
   },
   {
      "bought":false,
      "cost":-250,
      "effect":{
         "amount":2,
         "className":"com.example.slarocque.cellclicker.Research.ResearchEffects.ClickAmountEffectStatic"
      },
      "name":"Upgrade to Bacteria SuperFood"
   }
]

And this it the Research base class:

public class Research implements Serializable {
    public String name;
    public int cost;
    public boolean bought = false;
    public IResearchEffect effect;

    public Research() {super();}

    public Research(String _name, int _points, IResearchEffect _effect, Boolean _bought){
        super();
        this.name = _name;
        this.cost = _points;
        this.effect = _effect;
        this.bought = (_bought == null ? false : _bought);
    }

    public void IsComplete() {
        this.bought = true;
    }

    @Override
    public String toString() {
        return this.name + " - " + this.cost;
    }
}

Finally, this is how I'm trying to deserialize my Gson String:

    String json = settings.getString("List", null);
    List<Research> list = new ArrayList<>();

    //Make the GsonBuilder
    final GsonBuilder builder = new GsonBuilder();
    //builder.registerTypeAdapter(list.getClass(), /*Need adapter*/);

    final Gson gson = builder.create();
    Type listType = new TypeToken<List<Research>>() {}.getType();
    ResearchController.listResearch = gson.fromJson(json, listType);

Upvotes: 0

Views: 1813

Answers (2)

Lyubomyr Shaydariv
Lyubomyr Shaydariv

Reputation: 21105

You cannot deserialize interfaces directly unless you have enough information on how they must be instantiated. It's fine that you have the className field -- it may be enough to get everything. Since I have a local demo, not your classes, packages, etc, you can align the demo below to your code.

Nothing special here, just proof-of-concept using the getValue() method.

interface IResearchEffect {

    long getValue();

}

I consider the following custom mappings that must be adapted to yours:

final class ClickAmountEffectPercent
        implements IResearchEffect {

    final long percent = Long.valueOf(0);

    @Override
    public long getValue() {
        return percent;
    }

}
final class ClickAmountEffectStatic
        implements IResearchEffect {

    final long amount = Long.valueOf(0);

    @Override
    public long getValue() {
        return amount;
    }

}

Note that I'm using final PRIMITIVE_TYPE VAR = WRAPPER_TYPE.valueOf(DEFAULT_VALUE) here in order to disable primitive constant values inlining by javac. Similarly to the mappings above, here is the "top" mapping:

final class Research
        implements Serializable {

    final String name = null;
    final int cost = Integer.valueOf(0);
    final boolean bought = Boolean.valueOf(false);
    final IResearchEffect effect = null;

}

Now, the core part:

final class ResearchEffectTypeAdapterFactory
        implements TypeAdapterFactory {

    private static final TypeAdapterFactory researchEffectTypeAdapterFactory = new ResearchEffectTypeAdapterFactory();

    // Encapsulate the way it's instantiated
    private ResearchEffectTypeAdapterFactory() {
    }

    // ... not letting the caller to instantiate it with `new` -- it's a stateless singleton anyway, so one instance per application is FULLY legit
    static TypeAdapterFactory getResearchEffectTypeAdapterFactory() {
        return researchEffectTypeAdapterFactory;
    }

    @Override
    public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
        // Classes can be compared by == and !=
        // Note that we handle IResearchEffect only, otherwise we know that Gson has enought information itself
        if ( typeToken.getRawType() != IResearchEffect.class ) {
            return null;
        }
        // Create the type adapter for the IResearchEffect and cast it
        @SuppressWarnings("unchecked")
        final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) new MyTypeAdapter(gson);
        return typeAdapter;
    }

    private static final class MyTypeAdapter
            extends TypeAdapter<IResearchEffect> {

        private final Gson gson;

        private MyTypeAdapter(final Gson gson) {
            this.gson = gson;
        }

        @Override
        public void write(final JsonWriter out, final IResearchEffect value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public IResearchEffect read(final JsonReader in) {
            // Since readers and writers are one-use only, you have to buffer the current value in an in-memory JSON tree
            final JsonElement jsonElement = gson.fromJson(in, JsonElement.class);
            // Extract the className property
            final String className = jsonElement.getAsJsonObject().get("className").getAsString();
            // And resolve the instantiation class
            // Note that I'm using switch here because I use another packages for this demo and I have to remap the original document strings to my demo mappings
            // You have to use something like gson.from(jsonElement, Class.forName(className));
            // Or whatever you prefer, but I would extract it as a strategy
            switch ( className ) {
            case "com.example.slarocque.cellclicker.Research.ResearchEffects.ClickAmountEffectStatic":
                return gson.fromJson(jsonElement, ClickAmountEffectStatic.class);
            case "com.example.slarocque.cellclicker.Research.ResearchEffects.ClickAmountEffectPercent":
                return gson.fromJson(jsonElement, ClickAmountEffectPercent.class);
            default:
                throw new IllegalArgumentException("Cannot instantiate " + className);
            }
        }

    }

}

Demo:

// Note that TypeToken.getType() results can be considered value types thus being immutable and cached to a static final field
private static final Type researchesListType = new TypeToken<List<Research>>() {
}.getType();

// Gson is thread-safe as well, and can be used once per application
// Also, re-creating Gson instances would take more time due to its internals
private static final Gson gson = new GsonBuilder()
        .registerTypeAdapterFactory(getResearchEffectTypeAdapterFactory())
        .create();

public static void main(final String... args)
        throws IOException {
    try ( final Reader reader = getPackageResourceReader(Q43643447.class, "doc.json") ) {
        final List<Research> researches = gson.fromJson(reader, researchesListType);
        researches.forEach(research -> System.out.println(research.name + " " + research.effect.getValue()));
    }
}

Output:

Better Flasks 1
Buy a new Heater 120
Upgrade to Bacteria SuperFood 2

Upvotes: 3

Jonathan Aste
Jonathan Aste

Reputation: 1774

Try this:

Gson gson = new Gson();
ResearchController.listResearch = gson.fromJson(object.toString(), YourModel.class).getList();

YourModel should be a model with the element list inside it with a getter (getList()) of it.

Upvotes: 0

Related Questions