How to set super class properties when parsing JSON with gson - java

How to set super class "LookUp" properties id, name when parsing JSON array
of countries:
[ {"ID":5, "CountryNameEN":"UK" }, {"ID":6, "CountryNameEN":"USA" } ]
For example, When i calling get_lookups_countries() API with Retrofit 2 & parse the response with google Gson Library, I want to set super class instance members id & name with the same values of derived class "Country"
#GET(Constants.LookUps.GET_COUNTRIES) Call<List<Country>> get_lookups_countries();
Gson gson = new GsonBuilder()
.setLenient()
.registerTypeAdapter(LookUp.class,new LookupsDeserializer())
.create();
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient.Builder okHttpClient = new OkHttpClient.Builder();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient.build())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
return retrofit.create(APIEndpointVatTax.class);
public class LookUp {
int id;
String name;
}
public class Country extends LookUp {
#SerializedName("ID")
#Expose
private Integer iD;
#SerializedName("CountryNameEN")
#Expose
private String countryNameEN;
}

You seem to have some issues with your JSON mappings: you're trying to bind the super class fields to the sub class fields, however this is where an interface might be a better choice for you, because your intention is just asking the deserialized object for its id and name.
I would do it like this:
interface LookUp {
int getId();
String getName();
}
final class CountryByInterface
implements LookUp {
#SerializedName("ID")
private final Integer id = null;
#SerializedName("CountryNameEN")
private final String name = null;
#Override
public int getId() {
return id;
}
#Override
public String getName() {
return name;
}
}
So it could be used easily (Java 8 for the demo purposes only):
final Gson gson = new Gson();
final Type countryListType = new TypeToken<List<CountryByInterface>>() {
}.getType();
try ( final Reader reader = getPackageResourceReader(Q43247712.class, "countries.json") ) {
gson.<List<CountryByInterface>>fromJson(reader, countryListType)
.stream()
.map(c -> c.getId() + "=>" + c.getName())
.forEach(System.out::println);
}
If for some justified reason you really need the super class to hold such fields, you have to implement a post-processor (inspired with PostConstructAdapterFactory). Say,
abstract class AbstractLookUp {
int id;
String name;
abstract int getId();
abstract String getName();
final void postSetUp() {
id = getId();
name = getName();
}
}
final class CountryByClass
extends AbstractLookUp {
#SerializedName("ID")
private final Integer id = null;
#SerializedName("CountryNameEN")
private final String name = null;
#Override
int getId() {
return id;
}
#Override
String getName() {
return name;
}
}
final Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(new TypeAdapterFactory() {
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Check if it's a class we can handle: AbstractLookUp
if ( AbstractLookUp.class.isAssignableFrom(typeToken.getRawType()) ) {
// Get the downstream parser for the given type
final TypeAdapter<T> delegateTypeAdapter = gson.getDelegateAdapter(this, typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value)
throws IOException {
delegateTypeAdapter.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
// Deserialize it as an AbstractLookUp instance
final AbstractLookUp abstractLookUp = (AbstractLookUp) delegateTypeAdapter.read(in);
// And set it up
abstractLookUp.postSetUp();
#SuppressWarnings("unchecked")
final T result = (T) abstractLookUp;
return result;
}
};
}
return null;
}
})
.create();
final Type countryListType = new TypeToken<List<CountryByClass>>() {
}.getType();
try ( final Reader reader = getPackageResourceReader(Q43247712.class, "countries.json") ) {
gson.<List<CountryByClass>>fromJson(reader, countryListType)
.stream()
.map(c -> ((AbstractLookUp) c).id + "=>" + ((AbstractLookUp) c).name)
.forEach(System.out::println);
}
Both examples produce
5=>UK
6=>USA
However I find the first approach better designed and much easier to use, whereas the second one demonstrates how Gson can be configured to implement complex (de)serialization strategies.

Related

How to write custom serializer for proxy classes

I need to serialize/deserialize a POJO with enum. I have the following DTO:
public enum MyEnum {
VAL1("val1"),
VAL2("val2") {
#Override
public String getValue() {
return "test2";
}
};
private final String name;
MyEnum(String name) {
this.name = name;
}
public String getValue() {
return name;
}
}
public class MyPojo {
public MyEnum prop;
}
public static void main(String... args) {
Gson gson = new GsonBuilder().registerTypeAdapter(MyEnum.class, new MyEnumSeserializer());
MyPojo p = new MyPojo();
p.prop = MyEnum.VAL2; // and I get MyEnum$1.class and My serializer doesn't work
String json = gson.toJson(p);
MyPojo p1 = gson.fromJson(json, MyPojo.class);
}
How can I write a custom serializer/deserializer for proxy classes using Gson library? I can't use another library.
I've been found the solution. Need to change
new GsonBuilder().registerTypeAdapter(MyEnum.class, new MyEnumSeserializer());
to
new GsonBuilder(). registerTypeHierarchyAdapter(MyEnum.class, new MyEnumSeserializer());
and all work fine.

Convert String to ArrayList<String> using GSON

I am trying to deserialize a JSON data to a POJO.
The issue is that the list object is coming as a string, and gson gives an IllegalStateExceptioState. How can I parse the string as a list to an ArrayList using gson?
JSON DATA
{
"report_id":1943,
"history_id":3302654,
"project_id":null,
"owner_emails":"[\"abcd#xyz.com\"]",
"message":"Array\n(\n [name] => SOMENAME\n [age] => 36\n [gender] => male\n)\n"
}
POJO:
public class EventData {
private static Gson gson = new Gson();
#SerializedName("report_id")
public String reportID;
#SerializedName("history_id")
public String historyID;
#SerializedName("project_id")
public String projectID;
#SerializedName("owner_emails")
public ArrayList<String> ownerEmails = new ArrayList<String>();
#SerializedName("message")
public String message;
#SerializedName("title")
public String title;
public CrawlerNotifiedEventData(){
this.projectID = "Undefined";
this.reportID = "Undefined";
this.historyID = "Undefined";
this.title = "";
}
public String toJson(boolean base64Encode) throws java.io.UnsupportedEncodingException{
String json = gson.toJson(this, CrawlerNotifiedEventData.class);
if(base64Encode)
return Base64.getEncoder().encodeToString(json.getBytes("UTF8"));
return json;
}
public String toJson() throws java.io.UnsupportedEncodingException{
return this.toJson(false);
}
public static EventData builder(String json){
return gson.fromJson(json, EventData.class);
}
}
Deserialization:
EventData eventData = EventData.builder(json);
While deserializing i get the following error
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was STRING at line 1 column 252 path $.owner_emails
Boxing structured data in a string where it is unnecessary is a very common design issue across different serialization approaches. Fortunately, Gson can deal with fields like owner_emails (but not message of course).
Merely create a type adapter factory than can create a type adapter for a particular type by substituting the original one and doing a bit of more work. The adapter is supposed to read the payload as string and delegate the string deserialization to the type adapter it substitutes.
public final class JsonStringBoxTypeAdapterFactory
implements TypeAdapterFactory {
private JsonStringBoxTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final TypeAdapter<T> adapter = gson.getAdapter(typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value) {
throw new UnsupportedOperationException(); // TODO
}
#Override
public T read(final JsonReader in)
throws IOException {
return adapter.fromJson(in.nextString());
}
};
}
}
#AllArgsConstructor
#ToString
#EqualsAndHashCode
final class EventData {
#SerializedName("owner_emails")
#JsonAdapter(JsonStringBoxTypeAdapterFactory.class)
List<String> ownerEmails;
}
The unit test below will be green:
final EventData eventData = gson.fromJson(json, EventData.class);
Assertions.assertEquals(new EventData(ImmutableList.of("abcd#xyz.com")), eventData);
That's it.
"owner_emails" is curently a string as follows
"owner_emails":"[\"abcd#xyz.com\"]"
It should be
"owner_emails": ["abcd#xyz.com"]
to be considered as array. You can manually remove the quotes and parse it.
Or you can parse it using JsonElement in Gson
You can use ObjectMapper from jackson library for this conversion.
Sample code of conversion::
public <T> T mapResource(Object resource, Class<T> clazz) {
try {
return objectMapper.readValue(objectMapper.writeValueAsString(resource), clazz);
} catch (IOException ex) {
throw new Exception();
}
}
Modify the model for a list like::
public class Reportdata{
private List<String> owner_emails = new ArrayList();
#JsonDeserialize(contentAs = CustomClass.class)
private List<CustomClass> customClassList = new ArrayList();
....// setter and getter
}
In addition to this, while creating the ObjectMapper object you can pass or register the module/ your custom module for deserialization in object like below.
objectMapper.setDefaultPropertyInclusion(Include.NON_EMPTY);
objectMapper.disable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
objectMapper.registerModule(new JavaTimeModule());

Is there a function in java to check if any json attribute is null?

I have a message in JSON format that I converted to a JSONObject, and I have around 30 mandatory fields that I have to check for whether they're null or not. If one of these mandatory fields are null, I will discard the message, however other fields can be null without needing to discard the message. Is there any efficient way I can do this without going through each and every field and using isNull() ?
Also, the JSON objects are nested, so a simple anyNull() function would not work since it would only return if the object itself is null and not if the variables themselves are null.
I tried using gson to convert the message to a POJO, and created classes for 10 objects
Gson gson = new Gson();
Message message = gson.fromJson(msg, Message.class);
but since many classes are nested (and one of which is an array of objects) using simple null checkers don't work.
Actually speaking your question is not very clear because you're using a word of "message" that refers your particular class, but can also be more generic referring sent/received messages.
So something like for JSON elements in memory:
public static void failOnNullRecursively(final JsonElement jsonElement) {
if ( jsonElement.isJsonNull() ) {
throw new IllegalArgumentException("null!");
}
if ( jsonElement.isJsonPrimitive() ) {
return;
}
if ( jsonElement.isJsonArray() ) {
for ( final JsonElement element : jsonElement.getAsJsonArray() ) {
failOnNullRecursively(element);
}
return;
}
if ( jsonElement.isJsonObject() ) {
for ( final Map.Entry<String, JsonElement> e : jsonElement.getAsJsonObject().entrySet() ) {
failOnNullRecursively(e.getValue());
}
return;
}
throw new AssertionError(jsonElement);
}
or JSON documents in streams:
public final class FailOnNullJsonReader
extends JsonReader {
private FailOnNullJsonReader(final Reader reader) {
super(reader);
}
public static JsonReader create(final Reader reader) {
return new FailOnNullJsonReader(reader);
}
#Override
public void nextNull() {
throw new IllegalStateException(String.format("null at %#!", getPath()));
}
}
Both of them will throw on null. But it also seems that you want to validate Message instances:
If one of these mandatory fields are null, I will discard the message, however other fields can be null without needing to discard the message.
So this tells why the above null-checks won't fit your needs. What you're looking for is JSR-303. It won't be that efficient as you might want to want it to be (message instances are deserialized, validation takes time and resources too), but it might be efficient from the coding perspective:
final Set<ConstraintViolation<V>> violations = validator.validate(message);
if ( !violations.isEmpty() ) {
throw new ConstraintViolationException(violations);
}
or even integrate it right into Gson so that it serves middleware:
public final class PostReadTypeAdapterFactory<V>
implements TypeAdapterFactory {
private final Predicate<? super TypeToken<?>> supports;
private final BiConsumer<? super TypeToken<V>, ? super V> onRead;
private PostReadTypeAdapterFactory(final Predicate<? super TypeToken<?>> supports, final BiConsumer<? super TypeToken<V>, ? super V> onRead) {
this.supports = supports;
this.onRead = onRead;
}
public static <V> TypeAdapterFactory create(final Predicate<? super TypeToken<?>> supports, final BiConsumer<? super TypeToken<V>, ? super V> onRead) {
return new PostReadTypeAdapterFactory<>(supports, onRead);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !supports.test(typeToken) ) {
return null;
}
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, final T value)
throws IOException {
delegate.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
final T readValue = delegate.read(in);
#SuppressWarnings("unchecked")
final V value = (V) readValue;
#SuppressWarnings("unchecked")
final TypeToken<V> valueTypeToken = (TypeToken<V>) typeToken;
onRead.accept(valueTypeToken, value);
return readValue;
}
};
}
}
public final class Jsr303Support {
private Jsr303Support() {
}
public static <V> TypeAdapterFactory createTypeAdapterFactory(final Validator validator) {
return PostReadTypeAdapterFactory.<V>create(
typeToken -> typeToken.getRawType().isAnnotationPresent(Validate.class),
(typeToken, value) -> {
final Set<ConstraintViolation<V>> violations = validator.validate(value);
if ( !violations.isEmpty() ) {
throw new ConstraintViolationException(violations);
}
}
);
}
}
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
public #interface Validate {
}
And the test (using Lombok for brevity):
#Validate
#AllArgsConstructor
#EqualsAndHashCode
#ToString
final class Message {
#NotNull
final String foo;
#NotNull
final String bar;
#NotNull
final String baz;
}
public final class Jsr303SupportTest {
private static final Validator validator;
static {
try ( final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory() ) {
validator = validatorFactory.getValidator();
}
}
public static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.registerTypeAdapterFactory(Jsr303Support.createTypeAdapterFactory(validator))
.create();
#Test
public void test() {
Assertions.assertEquals(new Message("1", "2", "3"), gson.fromJson("{\"foo\":\"1\",\"bar\":\"2\",\"baz\":\"3\"}", Message.class));
final ConstraintViolationException ex = Assertions.assertThrows(ConstraintViolationException.class, () -> gson.fromJson("{\"foo\":\"1\",\"bar\":null,\"baz\":\"3\"}", Message.class));
Assertions.assertEquals(1, ex.getConstraintViolations().size());
}
}
And finally, probably the most efficient (in terms of reading JSON stream), but very limited whencompared to JSR-303 (and NOT working in Gson because Gson does not propagate null-checking to downstream (de)serializers), way that could replace #NotNull with a similar "functional" annotation:
public final class NotNullTypeAdapterFactory
implements TypeAdapterFactory {
// note no external access
private NotNullTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
final TypeAdapter<T> delegate = gson.getAdapter(typeToken);
return new TypeAdapter<T>() {
#Override
public void write(final JsonWriter out, #Nullable final T value)
throws IOException {
if ( value == null ) {
throw new IllegalArgumentException(typeToken + " with null");
}
delegate.write(out, value);
}
#Override
public T read(final JsonReader in)
throws IOException {
#Nullable
final T value = delegate.read(in);
if ( value == null ) {
throw new IllegalArgumentException(typeToken + " with null at " + in.getPath());
}
return value;
}
};
}
}
#AllArgsConstructor
#EqualsAndHashCode
#ToString
final class Message {
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String foo;
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String bar;
#JsonAdapter(NotNullTypeAdapterFactory.class)
final String baz;
}
public final class NotNullTypeAdapterFactoryTest {
public static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.create();
#Test
public void test() {
Assertions.assertEquals(new Message("1", "2", "3"), gson.fromJson("{\"foo\":\"1\",\"bar\":\"2\",\"baz\":\"3\"}", Message.class));
final IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> gson.fromJson("{\"foo\":\"1\",\"bar\":null,\"baz\":\"3\"}", Message.class));
Assertions.assertEquals("whatever here, the above does not work anyway", ex.getMessage());
}
}
The third, JSR-303, looks like the best for you.

Serialization of an Object having a polymorphic list of different type

I am trying to build a custom App to control Smart Home devices utilizing the Tasmota firmware and MQTT. Primarily that is smart lights at the moment. Therefore I have a Device class with children depending on the type of the device. Each device stores a DeviceState which is updated due to any changes made through the app. So the DeviceState is always the current state of the device. Depending on the device I need different DeviceStates, so again there is one superclass and subclass.
Now i want to store scenes with an ArrayList of DeviceStates to first store and than recreate certain light atmospheres. Therefore there is a class called Scene holding basic information and the described ArrayList.
To Store those lists i am using the Gson library in Android. My question is now how to be able to save those scenes objects with a polymorphic list inside.
I have followed this stackoverflow post: Gson serialize a list of polymorphic objects to save my devices as an Json String using the Gson library and a custom serializer / deserializer. But now Hence DeviceState doesn´t extend Scene I can´t use one serializer to create a String out of the Scene object. And if i would extend DeviceState to Scene, the DeviceState class would declare multiple JSON fields with the same name, because I am using "typeName" to differentiate those classes.
So basically i am getting the errors, that DeviceState doesn´t extend Scene or that DeviceState declares multiple JSON fields named "typeName".
public class Scene{
private ArrayList<DeviceState> states;
private String name;
private String room;
private String typeName;
public ArrayList<Scene> sceneList = new ArrayList<>();
public Scene(){
}
public Scene(String pName, String pRoom) {
name = pName;
room = pRoom;
states = new ArrayList<>();
typeName = "Scene";
}
public String getName() {
return name;
}
public String getRoom() {
return room;
}
public void addDevice(Device d) {
states.add(d.getState());
}
public void execute() {
System.out.println(states.size());
}
public String getTypeName(){
return typeName;
}
}
public class DeviceState {
private String typeName;
private String deviceTopic;
public static final String RGBWLightState = "RGBWLightState";
public static final String Default = "DeviceState";
public DeviceState(String pTypeName, String pDeviceTopic){
typeName = pTypeName;
deviceTopic = pDeviceTopic;
}
public DeviceState(){
typeName = Default;
}
public String getTypeName() {
return typeName;
}
}
public class CustomSceneSerializer implements JsonSerializer<ArrayList<Scene>> {
private static Map<String, Class> map = new TreeMap<>();
static {
map.put(DeviceState.RGBWLightState, RGBWLightState.class);
map.put(DeviceState.Default, DeviceState.class);
map.put("Scene", Scene.class);
}
#Override
public JsonElement serialize(ArrayList<Scene> src, Type typeOfSrc, JsonSerializationContext context) {
if(src == null) {
return null;
}else{
JsonArray ja = new JsonArray();
for(Scene s : src){
Class c = map.get(s.getTypeName());
if(c == null){
throw new RuntimeException("Unkown class: " + s.getTypeName());
}
ja.add(context.serialize(s, c));
}
return ja;
}
}
}
public class CustomSceneDeserializer implements JsonDeserializer<List<Scene>> {
private static Map<String, Class> map = new TreeMap<>();
static {
map.put(DeviceState.RGBWLightState, RGBWLightState.class);
map.put(DeviceState.Default, DeviceState.class);
map.put("Scene", Scene.class);
}
#Override
public List<Scene> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
ArrayList list = new ArrayList<Scene>();
JsonArray ja = json.getAsJsonArray();
for (JsonElement je : ja) {
String type = je.getAsJsonObject().get("typeName").getAsString();
Class c = map.get(type);
if (c == null)
throw new RuntimeException("Unknow class: " + type);
list.add(context.deserialize(je, c));
}
return list;
}
}
To save the Object holding a list of those objects I am using:
String json = preferences.getString("scene_holder", "");
GsonBuilder gb = new GsonBuilder();
List<Scene> al = new ArrayList<>();
gb.registerTypeAdapter(al.getClass(), new CustomSceneDeserializer());
gb.registerTypeAdapter(al.getClass(), new CustomSceneSerializer());
Gson gson = gb.create();
System.out.println(list.size());
list.get(0).execute();
System.out.println(json);
if (!(json.equals(""))) {
Scene result = gson.fromJson(json, Scene.class);
System.out.println(result.sceneList.size());
result.sceneList = list;
System.out.println(result.sceneList.size());
editor.putString("scene_holder", gson.toJson(result)).commit();
} else {
Scene scene = new Scene();
scene.sceneList = list;
editor.putString("scene_holder", gson.toJson(scene)).commit();
}

Using GSON with different items types [duplicate]

I'm trying to serialize/deserialize an object, that involves polymorphism, into JSON using Gson.
This is my code for serializing:
ObixBaseObj lobbyObj = new ObixBaseObj();
lobbyObj.setIs("obix:Lobby");
ObixOp batchOp = new ObixOp();
batchOp.setName("batch");
batchOp.setIn("obix:BatchIn");
batchOp.setOut("obix:BatchOut");
lobbyObj.addChild(batchOp);
Gson gson = new Gson();
System.out.println(gson.toJson(lobbyObj));
Here's the result:
{"obix":"obj","is":"obix:Lobby","children":[{"obix":"op","name":"batch"}]}
The serialization mostly works, except its missing the contents of inherited members (In particular obix:BatchIn and obixBatchout strings are missing).
Here's my base class:
public class ObixBaseObj {
protected String obix;
private String display;
private String displayName;
private ArrayList<ObixBaseObj> children;
public ObixBaseObj()
{
obix = "obj";
}
public void setName(String name) {
this.name = name;
}
...
}
Here's what my inherited class (ObixOp) looks like:
public class ObixOp extends ObixBaseObj {
private String in;
private String out;
public ObixOp() {
obix = "op";
}
public ObixOp(String in, String out) {
obix = "op";
this.in = in;
this.out = out;
}
public String getIn() {
return in;
}
public void setIn(String in) {
this.in = in;
}
public String getOut() {
return out;
}
public void setOut(String out) {
this.out = out;
}
}
I realize I could use an adapter for this, but the problem is that I'm serializing a collection of base class type ObixBaseObj. There are about 25 classes that inherits from this. How can I make this work elegantly?
There's a simple solution: Gson's RuntimeTypeAdapterFactory (from com.google.code.gson:gson-extras:$gsonVersion). You don't have to write any serializer, this class does all work for you. Try this with your code:
ObixBaseObj lobbyObj = new ObixBaseObj();
lobbyObj.setIs("obix:Lobby");
ObixOp batchOp = new ObixOp();
batchOp.setName("batch");
batchOp.setIn("obix:BatchIn");
batchOp.setOut("obix:BatchOut");
lobbyObj.addChild(batchOp);
RuntimeTypeAdapterFactory<ObixBaseObj> adapter =
RuntimeTypeAdapterFactory
.of(ObixBaseObj.class)
.registerSubtype(ObixBaseObj.class)
.registerSubtype(ObixOp.class);
Gson gson2=new GsonBuilder().setPrettyPrinting().registerTypeAdapterFactory(adapter).create();
Gson gson = new Gson();
System.out.println(gson.toJson(lobbyObj));
System.out.println("---------------------");
System.out.println(gson2.toJson(lobbyObj));
}
Output:
{"obix":"obj","is":"obix:Lobby","children":[{"obix":"op","name":"batch","children":[]}]}
---------------------
{
"type": "ObixBaseObj",
"obix": "obj",
"is": "obix:Lobby",
"children": [
{
"type": "ObixOp",
"in": "obix:BatchIn",
"out": "obix:BatchOut",
"obix": "op",
"name": "batch",
"children": []
}
]
}
EDIT: Better working example.
You said that there are about 25 classes that inherits from ObixBaseObj.
We start writing a new class, GsonUtils
public class GsonUtils {
private static final GsonBuilder gsonBuilder = new GsonBuilder()
.setPrettyPrinting();
public static void registerType(
RuntimeTypeAdapterFactory<?> adapter) {
gsonBuilder.registerTypeAdapterFactory(adapter);
}
public static Gson getGson() {
return gsonBuilder.create();
}
Every time we need a Gson object, instead of calling new Gson(), we will call
GsonUtils.getGson()
We add this code to ObixBaseObj:
public class ObixBaseObj {
protected String obix;
private String display;
private String displayName;
private String name;
private String is;
private ArrayList<ObixBaseObj> children = new ArrayList<ObixBaseObj>();
// new code
private static final RuntimeTypeAdapterFactory<ObixBaseObj> adapter =
RuntimeTypeAdapterFactory.of(ObixBaseObj.class);
private static final HashSet<Class<?>> registeredClasses= new HashSet<Class<?>>();
static {
GsonUtils.registerType(adapter);
}
private synchronized void registerClass() {
if (!registeredClasses.contains(this.getClass())) {
registeredClasses.add(this.getClass());
adapter.registerSubtype(this.getClass());
}
}
public ObixBaseObj() {
registerClass();
obix = "obj";
}
Why? because every time this class or a children class of ObixBaseObj is instantiated,
the class it's gonna be registered in the RuntimeTypeAdapter
In the child classes, only a minimal change is needed:
public class ObixOp extends ObixBaseObj {
private String in;
private String out;
public ObixOp() {
super();
obix = "op";
}
public ObixOp(String in, String out) {
super();
obix = "op";
this.in = in;
this.out = out;
}
Working example:
public static void main(String[] args) {
ObixBaseObj lobbyObj = new ObixBaseObj();
lobbyObj.setIs("obix:Lobby");
ObixOp batchOp = new ObixOp();
batchOp.setName("batch");
batchOp.setIn("obix:BatchIn");
batchOp.setOut("obix:BatchOut");
lobbyObj.addChild(batchOp);
Gson gson = GsonUtils.getGson();
System.out.println(gson.toJson(lobbyObj));
}
Output:
{
"type": "ObixBaseObj",
"obix": "obj",
"is": "obix:Lobby",
"children": [
{
"type": "ObixOp",
"in": "obix:BatchIn",
"out": "obix:BatchOut",
"obix": "op",
"name": "batch",
"children": []
}
]
}
I hope it helps.
I think that a custom serializer/deserializer is the only way to proceed and I tried to propose you the most compact way to realize it I have found. I apologize for not using your classes, but the idea is the same (I just wanted at least 1 base class and 2 extended classes).
BaseClass.java
public class BaseClass{
#Override
public String toString() {
return "BaseClass [list=" + list + ", isA=" + isA + ", x=" + x + "]";
}
public ArrayList<BaseClass> list = new ArrayList<BaseClass>();
protected String isA="BaseClass";
public int x;
}
ExtendedClass1.java
public class ExtendedClass1 extends BaseClass{
#Override
public String toString() {
return "ExtendedClass1 [total=" + total + ", number=" + number
+ ", list=" + list + ", isA=" + isA + ", x=" + x + "]";
}
public ExtendedClass1(){
isA = "ExtendedClass1";
}
public Long total;
public Long number;
}
ExtendedClass2.java
public class ExtendedClass2 extends BaseClass{
#Override
public String toString() {
return "ExtendedClass2 [total=" + total + ", list=" + list + ", isA="
+ isA + ", x=" + x + "]";
}
public ExtendedClass2(){
isA = "ExtendedClass2";
}
public Long total;
}
CustomDeserializer.java
public class CustomDeserializer implements JsonDeserializer<List<BaseClass>> {
private static Map<String, Class> map = new TreeMap<String, Class>();
static {
map.put("BaseClass", BaseClass.class);
map.put("ExtendedClass1", ExtendedClass1.class);
map.put("ExtendedClass2", ExtendedClass2.class);
}
public List<BaseClass> deserialize(JsonElement json, Type typeOfT,
JsonDeserializationContext context) throws JsonParseException {
List list = new ArrayList<BaseClass>();
JsonArray ja = json.getAsJsonArray();
for (JsonElement je : ja) {
String type = je.getAsJsonObject().get("isA").getAsString();
Class c = map.get(type);
if (c == null)
throw new RuntimeException("Unknow class: " + type);
list.add(context.deserialize(je, c));
}
return list;
}
}
CustomSerializer.java
public class CustomSerializer implements JsonSerializer<ArrayList<BaseClass>> {
private static Map<String, Class> map = new TreeMap<String, Class>();
static {
map.put("BaseClass", BaseClass.class);
map.put("ExtendedClass1", ExtendedClass1.class);
map.put("ExtendedClass2", ExtendedClass2.class);
}
#Override
public JsonElement serialize(ArrayList<BaseClass> src, Type typeOfSrc,
JsonSerializationContext context) {
if (src == null)
return null;
else {
JsonArray ja = new JsonArray();
for (BaseClass bc : src) {
Class c = map.get(bc.isA);
if (c == null)
throw new RuntimeException("Unknow class: " + bc.isA);
ja.add(context.serialize(bc, c));
}
return ja;
}
}
}
and now this is the code I executed to test the whole thing:
public static void main(String[] args) {
BaseClass c1 = new BaseClass();
ExtendedClass1 e1 = new ExtendedClass1();
e1.total = 100L;
e1.number = 5L;
ExtendedClass2 e2 = new ExtendedClass2();
e2.total = 200L;
e2.x = 5;
BaseClass c2 = new BaseClass();
c1.list.add(e1);
c1.list.add(e2);
c1.list.add(c2);
List<BaseClass> al = new ArrayList<BaseClass>();
// this is the instance of BaseClass before serialization
System.out.println(c1);
GsonBuilder gb = new GsonBuilder();
gb.registerTypeAdapter(al.getClass(), new CustomDeserializer());
gb.registerTypeAdapter(al.getClass(), new CustomSerializer());
Gson gson = gb.create();
String json = gson.toJson(c1);
// this is the corresponding json
System.out.println(json);
BaseClass newC1 = gson.fromJson(json, BaseClass.class);
System.out.println(newC1);
}
This is my execution:
BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0]
{"list":[{"total":100,"number":5,"list":[],"isA":"ExtendedClass1","x":0},{"total":200,"list":[],"isA":"ExtendedClass2","x":5},{"list":[],"isA":"BaseClass","x":0}],"isA":"BaseClass","x":0}
BaseClass [list=[ExtendedClass1 [total=100, number=5, list=[], isA=ExtendedClass1, x=0], ExtendedClass2 [total=200, list=[], isA=ExtendedClass2, x=5], BaseClass [list=[], isA=BaseClass, x=0]], isA=BaseClass, x=0]
Some explanations: the trick is done by another Gson inside the serializer/deserializer. I use just isA field to spot the right class. To go faster, I use a map to associate the isA string to the corresponding class. Then, I do the proper serialization/deserialization using the second Gson object. I declared it as static so you won't slow serialization/deserialization with multiple allocation of Gson.
Pro
You actually do not write more code than this, you let Gson do all the work. You have just to remember to put a new subclass into the maps (the exception reminds you of that).
Cons
You have two maps. I think that my implementation can refined a bit to avoid map duplications, but I left them to you (or to future editor, if any).
Maybe you want to unify serialization and deserialization into a unique object, you should be check the TypeAdapter class or experiment with an object that implements both interfaces.
I appreciate the other answers here that led me on my path to solving this issue. I used a combination of RuntimeTypeAdapterFactory with Reflection.
I also created a helper class to make sure a properly configured Gson was used.
Within a static block inside the GsonHelper class, I have the following code go through my project to find and register all of the appropriate types. All of my objects that will go through JSON-ification are a subtype of Jsonable.
You will want to change the following:
my.project in Reflections should be your package name.
Jsonable.class is my base class. Substitute yours.
I like having the field show the full canonical name, but clearly if you don't want / need it, you can leave out that part of the call to register the subtype. The same thing goes for className in the RuntimeAdapterFactory; I have data items already using the type field.
private static final GsonBuilder gsonBuilder = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
.excludeFieldsWithoutExposeAnnotation()
.setPrettyPrinting();
static {
Reflections reflections = new Reflections("my.project");
Set<Class<? extends Jsonable>> allTypes = reflections.getSubTypesOf(Jsonable.class);
for (Class< ? extends Jsonable> serClass : allTypes){
Set<?> subTypes = reflections.getSubTypesOf(serClass);
if (subTypes.size() > 0){
RuntimeTypeAdapterFactory<?> adapterFactory = RuntimeTypeAdapterFactory.of(serClass, "className");
for (Object o : subTypes ){
Class c = (Class)o;
adapterFactory.registerSubtype(c, c.getCanonicalName());
}
gsonBuilder.registerTypeAdapterFactory(adapterFactory);
}
}
}
public static Gson getGson() {
return gsonBuilder.create();
}
I created a type adapter factory that uses an annotation and ClassGraph to discover subclasses and supports multiple serialization styles (Type Property, Property, Array). See github for source code and maven coordinates.

Categories

Resources