Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Gson Type Adapter vs. Custom Deseralizer

Tags:

java

gson

The example below shows a class (Club) that contains a collection of an abstract class (Member). I'm confused as to whether I need a TypeAdapter or JsonDeserializer to make the Deserialization work correctly. Serialization works just fine without any help, but Deserialization is throwing exceptions. To illustrate I've built the following "clone" test. If anyone could show a working example I would be very grateful.

First Club Class

package gson.test;
import java.util.ArrayList;

import com.google.gson.Gson;

public class Club {
    public static void main(String[] args) {
        // Setup a Club with 2 members
        Club myClub = new Club();
        myClub.addMember(new Silver());
        myClub.addMember(new Gold());

        // Serialize to JSON
        Gson gson = new Gson();
        String myJsonClub = gson.toJson(myClub); 
        System.out.println(myJsonClub);

        // De-Serialize to Club
        Club myNewClub = gson.fromJson(myJsonClub, Club.class);
        System.out.println(myClub.equals(myNewClub) ? "Cloned!" : "Failed");
    }

    private String title = "MyClub";
    private ArrayList<Member> members = new ArrayList<Member>();

    public boolean equals(Club that) {
        if (!this.title.equals(that.title)) return false;
        for (int i=0; i<this.members.size(); i++) {
            if (! this.getMember(i).equals(that.getMember(i))) return false;
        }
        return true;
    }
    public void addMember(Member newMember) { members.add(newMember); }
    public Member getMember(int i) { return members.get(i); }
}

Now the Abstract Base Class Member

package gson.test;
public abstract class Member {
    private int type;
    private String name = "";

    public int getType() { return type; }
    public void setType(int type) { this.type = type; }
    public boolean equals(Member that) {return this.name.equals(that.name);}
}

And two concrete sub-classes of Member (Gold and Silver)

package gson.test;
public class Gold extends Member {
    private String goldData = "SomeGoldData";
    public Gold() {
        super();
        this.setType(2);
    }
    public boolean equals(Gold that) {
        return (super.equals(that) && this.goldData.equals(that.goldData)); 
    }
}

package gson.test;
public class Silver extends Member {
    private String silverData = "SomeSilverData";
    public Silver() {
        super();
        this.setType(1);
    }
    public boolean equals(Silver that) { 
        return (super.equals(that) && this.silverData.equals(that.silverData)); 
    }
}

And finally the output

    {"title":"MyClub","members":[{"silverData":"SomeSilverData","type":1,"name":""},{"goldData":"SomeGoldData","type":2,"name":""}]}
    Exception in thread "main" java.lang.RuntimeException: Failed to invoke public gson.test.Member() with no args
        at com.google.gson.internal.ConstructorConstructor$3.construct(ConstructorConstructor.java:107)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:186)
...
like image 783
Mike Storey Avatar asked Jun 03 '15 21:06

Mike Storey


People also ask

Which of the following GSON methods are useful for implementing JSON?

Gson is first constructed using GsonBuilder and then, toJson(Object) or fromJson(String, Class) methods are used to read/write JSON constructs.

How do I register a type GSON adapter?

To use a custom type adapter with Gson, you must register it with a GsonBuilder : GsonBuilder builder = new GsonBuilder(); builder. registerTypeAdapter(Point. class, new PointAdapter()); // if PointAdapter didn't check for nulls in its read/write methods, you should instead use // builder.

What is GSON toJson in Java?

Introduction. Gson is the main actor class of Google Gson library. It provides functionalities to convert Java objects to matching JSON constructs and vice versa. Gson is first constructed using GsonBuilder and then toJson(Object) or fromJson(String, Class) methods are used to read/write JSON constructs.


2 Answers

You can do both. Which one you pick depends really on potential performance impact, and how much code are willing to write.

Deserializers are more expensive. That is because the input to deserializer is a json tree, and GSon will have to create a full JsonElement subtree for the element that matches your class, before it can pass it to your deserializer. If your model has a lot of nesting, that cost increases. For plain objects, it will be negligible.

It seems that you will know which class to create based on the value of type property that will be included in target object. Your deserializer will need to

  • look into the passed JsonElement object, read the type property, determine the type
  • call context.deserialize() with the class and the same element that was passed to you
  • throw an error if type was missing or invalid

Your type adapter will have to be more complex. The input to the type adapter is a stream, not an element/subtree. You can load the next value entirely from the stream, parse it, and then do exactly what deserializer did, which doesn't make sense and you can just use the deserializer instead. Alternatively, you can read the stream, see what properties there are, save them into local variables, until you get to the type property (you can't predict its location), then finish reading the remainder of the properties, and create your final Gold/Silver objects based on type, and all the properties read and saved.

like image 172
Pawel Veselov Avatar answered Oct 06 '22 10:10

Pawel Veselov


Ok, real working example (I'm pretty sure this time).

The Club

package gson.test;
import java.util.ArrayList;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Club {
    public static void main(String[] args) {
        // Setup a Club with 2 members
        Club myClub = new Club();
        myClub.addMember(new Silver("Jack"));
        myClub.addMember(new Gold("Jill"));
        myClub.addMember(new Silver("Mike"));

        // Get the GSON Object and register Type Adapter
        GsonBuilder builder = new GsonBuilder();
        builder.registerTypeAdapter(Member.class, new MemberDeserializer());
        builder.registerTypeAdapter(Member.class, new MemberSerializer());
        builder.setPrettyPrinting();
        Gson gson = builder.create();

        // Serialize Club to JSON
        String myJsonClub = gson.toJson(myClub); 

        // De-Serialize to Club
        Club myNewClub = gson.fromJson(myJsonClub, Club.class);
        System.out.println(myClub.equals(myNewClub) ? "Cloned!" : "Failed");
        System.out.println(gson.toJson(myNewClub));
    }

    private String title = "MyClub";
    private ArrayList<Member> members = new ArrayList<Member>();

    public boolean equals(Object club) {
        Club that = (Club) club;
        if (!this.title.equals(that.title)) return false;
        for (int i=0; i<this.members.size(); i++) {
            Member member1 = this.getMember(i);
            Member member2 = that.getMember(i);
            if (! member1.equals(member2)) return false;
        }
        return true;
    }
    public void addMember(Member newMember) { members.add(newMember); }
    public Member getMember(int i) { return members.get(i); }
}

The Member Abstract Class

package gson.test;
public abstract class Member {
    private String clsname = this.getClass().getName() ;
    private int type;
    private String name = "unknown";

    public Member() { }
    public Member(String theName) {this.name = theName;}
    public int getType() { return type; }
    public void setType(int type) { this.type = type; }
    public boolean equals(Object member) {
        Member that = (Member) member;
        return this.name.equals(that.name);
    }
}

The Concrete Sub-Classes Silver and Gold

package gson.test;
public class Silver extends Member {
    private String silverData = "SomeSilverData";
    public Silver() { 
        super(); 
        this.setType(1); 
    }
    public Silver(String theName) {
        super(theName); 
        this.setType(1); 
    }
    public boolean equals(Object that) {
        Silver silver = (Silver)that;
        return (super.equals(that) && this.silverData.equals(silver.silverData)); 
    }
}

package gson.test;
public class Gold extends Member {
    private String goldData = "SomeGoldData";
    private String extraData = "Extra Gold Data";
    public Gold() {
        super(); 
        this.setType(2);
    }
    public Gold(String theName) { 
        super(theName); 
        this.setType(2); 
    }
    public boolean equals(Gold that) {
        Gold gold = (Gold) that;
        return (super.equals(that) && this.goldData.equals(gold.goldData)); 
    }
}

The Custom Member Serailizer

package gson.test;
import java.lang.reflect.Type;
import com.google.gson.JsonElement;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;

public class MemberSerializer implements JsonSerializer<Member> {

    public JsonElement serialize(Member src, Type member, JsonSerializationContext context) {
        switch (src.getType()) {
            case 1: return context.serialize((Silver)src);
            case 2: return context.serialize((Gold)src);
            default: return null;
        }
    }
}

The custom Deserializer

package gson.test;
import java.lang.reflect.Type;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;

public class MemberDeserializer implements JsonDeserializer<Member> {
    @Override
    public Member deserialize(JsonElement json, Type member, JsonDeserializationContext context) {
        int myType = json.getAsJsonObject().get("type").getAsInt();
        switch (myType) {
            case 1: return context.deserialize(json, Silver.class);
            case 2: return context.deserialize(json, Gold.class);
            default: return null;
        }
    }
}

And... the output

Cloned!
{
  "title": "MyClub",
  "members": [
    {
      "silverData": "SomeSilverData",
      "clsname": "gson.test.Silver",
      "type": 1,
      "name": "Jack"
    },
    {
      "goldData": "SomeGoldData",
      "extraData": "Extra Gold Data",
      "clsname": "gson.test.Gold",
      "type": 2,
      "name": "Jill"
    },
    {
      "silverData": "SomeSilverData",
      "clsname": "gson.test.Silver",
      "type": 1,
      "name": "Mike"
    }
  ]
}

I should note that my real use-case is one where performance should not be an issue, I'm loading a cache of objects from jSon text files so the frequency with this code is executed makes performance much less important than maintainability.

like image 38
Mike Storey Avatar answered Oct 06 '22 08:10

Mike Storey