1

I am running into an odd situation with GSON polymorphic objects in my current project. The situation is this: I have two different abstract base classes with two different use cases:

  • the first class is contained within a List in and of itself, and
  • the second class is also contained within a List, but the list is part of a larger parent object

Simplified versions of contrived classes (constructors and accessors left out for brevity; discriminator fields defined but commented out for illustration):

public abstract class ClassNumeric {
    //private String numericType;
}

public class ClassOne extends ClassNumeric {
   private String hex;
}

public class ClassTwo extends ClassNumeric {
    private Integer whole;
    private Integer fraction;
}

public abstract class ClassAlphabetic {
    //private String alphabeticType;
}

public class ClassAlpha extends ClassAlphabetic {
    private String name;
}

public class ClassBravo extends ClassAlphabetic {
    private Integer age;
    private Integer numberOfMarbles;
}

public class Container {
    private String group;
    private List<ClassAlphabetic> alphabetics;
}

Adapter factories and their registrations with GSON:

public RuntimeTypeAdapterFactory<ClassNumeric> numericTypeFactory = RuntimeTypeAdapterFactory
    .of(ClassNumeric.class, "numericType")
    .registerSubtype(ClassOne.class)
    .registerSubtype(ClassTwo.class);

public RuntimeTypeAdapterFactory<ClassAlphabetic> alphabeticTypeFactory = RuntimeTypeAdapterFactory
    .of(ClassAlphabetic.class, "alphabeticType")
    .registerSubtype(ClassAlpha.class)
    .registerSubtype(ClassBravo.class);

public final Gson gson = new GsonBuilder()
    .setPrettyPrinting()
    .disableHtmlEscaping()
    .registerTypeAdapterFactory(numericTypeFactory)
    .registerTypeAdapterFactory(alphabeticTypeFactory)
    .create();

Based on what I've read so far, I don't have to (and actually aren't supposed to) declare the discriminator field in the base classes because GSON handles those internally as the JSON is serialized and deserialized.

Here is an example of how these can be used:

ClassOne c1 = ClassOne.builder().hex("EF8A").build();
ClassTwo c2 = ClassTwo.builder().whole(1).fraction(3).build();
List<ClassNumeric> numerics = Arrays.asList(c1, c2);   // List of child objects
log.debug("Numerics: " + gson.toJson(numerics));

ClassAlpha ca = ClassAlpha.builder().name("Fred").build(); 
ClassBravo cb = ClassBravo.builder().age(5).numberOfMarbles(42).build();
List<ClassAlphabetic> alphas = Arrays.asList(ca, cb);
Container container = Container.builder().group("Test Group 1").alphabetics(alpha).build();  // List of objects field on larger object
log.debug("Alphas (container): " + gson.toJson(container));

The problem I'm having is that the ClassAlphabetic objects work fine (the discriminator field is present in the JSON), while the ClassNumeric objects do not (discriminator field missing). Sample output:

09:12:17.910 [main] DEBUG c.s.s.s.s.GSONPolymorphismTest - Numerics: [
    {
        "hex": "EF8A"
    },
    {
        "whole": 1,
        "fraction": 3
    }
]
09:12:17.926 [main] DEBUG c.s.s.s.s.GSONPolymorphismTest - Alphas (container): {
    "group": "Test Group 1",
    "alphabetics": [
        {
            "alphabeticType": "ClassAlpha",
            "name": "Fred"
        },
        {
            "alphabeticType": "ClassBravo",
            "age": 5,
            "numberOfMarbles": 42
        }
    ]
}

What am I missing here? These are essentially defined and set up with GSON the same way, but one use case works where the other doesn't.

TheIcemanCometh
  • 996
  • 2
  • 15
  • 29

1 Answers1

1

This is because of how generics work in Java. In short, a particular generic class instance does not have any type parameterization information as a part of the instance. Type parameters, however, can be stored in field types, method return types, methods parameters, inherited super classes (say, extends ArrayList<Integer>), custom parameterized type information instances, etc. Local variables, like what numerics are, keep type parameters during compile time and exist in your and compiler minds -- due to type erasure. So, it's like a raw List in runtime. Similarly tonumerics, alphas does not have any runtime parameterization, but, unlike local variables, the Container.alphabetics field has type information that's preserved in runtime -- fields can provide type information in full. And this is used by Gson to determine which (de)serialization strategy to apply. Similarly, Gson uses default strategies when there is no additional type parameter information provided (like for the local variables). As I mentioned above, you can also create a custom parameterized type ParameterizedType to provide raw type and its type parameters information. How it can help? If you take a closer look at Gson toJson overloads, you can see that one of its overloads accepts an additional parameter (I've chosed the simplest one). This can be considered a sort of hint to Gson to tell it the exact type of the passed instance (not necessarily matches, but it should in very most cases). So, just to make it work, tell Gson your numerics List type parameterization:

// "Canonical" way: TypeToken analyzes its superclass parameterization and returns its via the getType method
private static final Type classNumericListType = new TypeToken<List<ClassNumeric>>() {
}.getType()));

System.out.println("Numerics: " + gson.toJson(numerics, classNumericListType);

Or, if you can use Gson 2.8.0+,

private static final Type classNumericListType = TypeToken.getParameterized(List.class, ClassNumeric.class);

System.out.println("Numerics: " + gson.toJson(numerics, classNumericListType);

Or simply create your own ParameterizedType implementation. I would go with type tokens when the type is known at compile time, and TypeToken.getParameterized if the type is only known at runtime. Having that, passing the type instance triggers the RuntimeTypeAdapterFactory mechanism (now it's ClassNumeric, and not raw Object), so the output is as follows:

Numerics: [
  {
    "numericType": "N1",
    "hex": "EF8A"
  },
  {
    "numericType": "N2",
    "whole": 1,
    "fraction": 3
  }
]
Lyubomyr Shaydariv
  • 18,039
  • 11
  • 53
  • 97