gson - How to include class name property when serializing object of any type

Yet another answer. It took a bit longer.

Side comment: The above solution would work if you would recursively use reflection to work out the fields in your class. Then serialise those with the special serialiser, while using a separate one for the parent object. This would avoid the stackoverflow.

Having said that - i am a lazy developer, so I like to do things lazy. I am adapting a google solution for you.

NOTE: PLEASE TEST THIS AND ADAPT IT TO YOUR NEEDS. THIS IS A PROTOTYPE AND I HAVE NOT CLEANED UP UNNECESSARY CODE OR CHECKED FOR POSSIBLE ISSUES>

The original source of the code:

https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java

So, this is based on the RuntimeTypeAdapterFactory. This factory is provided by google and its purpose is to support hierarchical deserialisation. To do this, you would register a base class and ALL subclasses, with a property that you would like to add as an identifier. If you read the javadocs, this would get much clearer.

This obviously offers us the thing that we want: recursively register different adapters for class types that can handle these, while NOT running in circles and causing a stackoverflow. With one important issue: you have to register ALL subclasses. This is obviously not suitable (though one might argue you could you classpath resolution and simply add all your classes at startup once to be able to use this everywhere). So I looked into the source and changed the code to do this dynamically. Note that google warns against doing it - use it on your own terms :)

Here is my Factory:

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

/**
 * Adapts values whose runtime type may differ from their declaration type. This
 * is necessary when a field's type is not the same type that GSON should create
 * when deserializing that field. For example, consider these types:
 * <pre>   {@code
 *   abstract class Shape {
 *     int x;
 *     int y;
 *   }
 *   class Circle extends Shape {
 *     int radius;
 *   }
 *   class Rectangle extends Shape {
 *     int width;
 *     int height;
 *   }
 *   class Diamond extends Shape {
 *     int width;
 *     int height;
 *   }
 *   class Drawing {
 *     Shape bottomShape;
 *     Shape topShape;
 *   }
 * }</pre>
 * <p>Without additional type information, the serialized JSON is ambiguous. Is
 * the bottom shape in this drawing a rectangle or a diamond? <pre>   {@code
 *   {
 *     "bottomShape": {
 *       "width": 10,
 *       "height": 5,
 *       "x": 0,
 *       "y": 0
 *     },
 *     "topShape": {
 *       "radius": 2,
 *       "x": 4,
 *       "y": 1
 *     }
 *   }}</pre>
 * This class addresses this problem by adding type information to the
 * serialized JSON and honoring that type information when the JSON is
 * deserialized: <pre>   {@code
 *   {
 *     "bottomShape": {
 *       "type": "Diamond",
 *       "width": 10,
 *       "height": 5,
 *       "x": 0,
 *       "y": 0
 *     },
 *     "topShape": {
 *       "type": "Circle",
 *       "radius": 2,
 *       "x": 4,
 *       "y": 1
 *     }
 *   }}</pre>
 * Both the type field name ({@code "type"}) and the type labels ({@code
 * "Rectangle"}) are configurable.
 *
 * <h3>Registering Types</h3>
 * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
 * name to the {@link #of} factory method. If you don't supply an explicit type
 * field name, {@code "type"} will be used. <pre>   {@code
 *   RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
 *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
 * }</pre>
 * Next register all of your subtypes. Every subtype must be explicitly
 * registered. This protects your application from injection attacks. If you
 * don't supply an explicit type label, the type's simple name will be used.
 * <pre>   {@code
 *   shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
 *   shapeAdapter.registerSubtype(Circle.class, "Circle");
 *   shapeAdapter.registerSubtype(Diamond.class, "Diamond");
 * }</pre>
 * Finally, register the type adapter factory in your application's GSON builder:
 * <pre>   {@code
 *   Gson gson = new GsonBuilder()
 *       .registerTypeAdapterFactory(shapeAdapterFactory)
 *       .create();
 * }</pre>
 * Like {@code GsonBuilder}, this API supports chaining: <pre>   {@code
 *   RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
 *       .registerSubtype(Rectangle.class)
 *       .registerSubtype(Circle.class)
 *       .registerSubtype(Diamond.class);
 * }</pre>
 */
public final class RuntimeClassNameTypeAdapterFactory<T> implements TypeAdapterFactory {
  private final Class<?> baseType;
  private final String typeFieldName;
  private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
  private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();

  private RuntimeClassNameTypeAdapterFactory(Class<?> baseType, String typeFieldName) {
    if (typeFieldName == null || baseType == null) {
      throw new NullPointerException();
    }
    this.baseType = baseType;
    this.typeFieldName = typeFieldName;
  }

  /**
   * Creates a new runtime type adapter using for {@code baseType} using {@code
   * typeFieldName} as the type field name. Type field names are case sensitive.
   */
  public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
    return new RuntimeClassNameTypeAdapterFactory<T>(baseType, typeFieldName);
  }

  /**
   * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
   * the type field name.
   */
  public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType) {
    return new RuntimeClassNameTypeAdapterFactory<T>(baseType, "class");
  }

  /**
   * Registers {@code type} identified by {@code label}. Labels are case
   * sensitive.
   *
   * @throws IllegalArgumentException if either {@code type} or {@code label}
   *     have already been registered on this type adapter.
   */
  public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
    if (type == null || label == null) {
      throw new NullPointerException();
    }
    if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
      throw new IllegalArgumentException("types and labels must be unique");
    }
    labelToSubtype.put(label, type);
    subtypeToLabel.put(type, label);
    return this;
  }

  /**
   * Registers {@code type} identified by its {@link Class#getSimpleName simple
   * name}. Labels are case sensitive.
   *
   * @throws IllegalArgumentException if either {@code type} or its simple name
   *     have already been registered on this type adapter.
   */
  public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
    return registerSubtype(type, type.getSimpleName());
  }

  public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {

    final Map<String, TypeAdapter<?>> labelToDelegate
        = new LinkedHashMap<String, TypeAdapter<?>>();
    final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
        = new LinkedHashMap<Class<?>, TypeAdapter<?>>();

//    && !String.class.isAssignableFrom(type.getRawType())

    if(Object.class.isAssignableFrom(type.getRawType()) ) {
        TypeAdapter<?> delegate = gson.getDelegateAdapter(this, type);
        labelToDelegate.put("class", delegate);
        subtypeToDelegate.put(type.getRawType(), delegate);
    }

//    for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
//      TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
//      labelToDelegate.put(entry.getKey(), delegate);
//      subtypeToDelegate.put(entry.getValue(), delegate);
//    }

    return new TypeAdapter<R>() {
      @Override public R read(JsonReader in) throws IOException {
        JsonElement jsonElement = Streams.parse(in);
        JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
        if (labelJsonElement == null) {
          throw new JsonParseException("cannot deserialize " + baseType
              + " because it does not define a field named " + typeFieldName);
        }
        String label = labelJsonElement.getAsString();
        @SuppressWarnings("unchecked") // registration requires that subtype extends T
        TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
        if (delegate == null) {
          throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
              + label + "; did you forget to register a subtype?");
        }
        return delegate.fromJsonTree(jsonElement);
      }

      @Override public void write(JsonWriter out, R value) throws IOException {
        Class<?> srcType = value.getClass();
        String label = srcType.getName();
        @SuppressWarnings("unchecked") // registration requires that subtype extends T
        TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
        if (delegate == null) {
          throw new JsonParseException("cannot serialize " + srcType.getName()
              + "; did you forget to register a subtype?");
        }
        JsonElement jsonTree = delegate.toJsonTree(value);
        if(jsonTree.isJsonPrimitive()) {
            Streams.write(jsonTree, out);
        } else {
            JsonObject jsonObject = jsonTree.getAsJsonObject();
            if (jsonObject.has(typeFieldName)) {
              throw new JsonParseException("cannot serialize " + srcType.getName()
                  + " because it already defines a field named " + typeFieldName);
            }
            JsonObject clone = new JsonObject();
            clone.add(typeFieldName, new JsonPrimitive(label));
            for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
              clone.add(e.getKey(), e.getValue());
            }
            Streams.write(clone, out);
        }
      }
    }.nullSafe();
  }
}

I have added ALL imports for you. This is not (really) published in maven central, though you could find it here: https://mvnrepository.com/artifact/org.danilopianini/gson-extras/0.1.0

Regardless you would have to make adaptions to get this working for you, so I made a copy. The copy fully compiles and you can simply paste it into your code and save yourself the extra dependency.

The important bits of this code are as follows: (and I have purposely left them in but commented out so you can tell)

in create(Gson gson, TypeToken<R> type)

Check if the raw type is assignable from the String class. You want this to be applied to every class object, so this takes care of that. Note the code before that would look up if the type is registered with the class - not needed anymore (accordingly the variables wouldn't be needed; you should clean up the code)

in @Override public void write(JsonWriter out, R value) throws IOException {:

First, we get rid of the label. Our label is and will always be the name of the source type. This is done in:

String label = srcType.getName();

Second, we have to make a distinction between primitive and object types. Primitive types are Strings, Integers etc in the Gson world. This means that our check above (adding an adapter) does not catch the fact that these Object types are in deed primitive types. So we do:

if(jsonTree.isJsonPrimitive()) {
            Streams.write(jsonTree, out);

This takes care of that. If it is primitive, just write the tree into the stream. If it is not, we then write all other fields AND the class field into it.

JsonObject clone = new JsonObject();
            clone.add(typeFieldName, new JsonPrimitive(label));
            for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
              clone.add(e.getKey(), e.getValue());
            }
            Streams.write(clone, out);

Fewww - finally this now takes care of that. And here is the example to prove my code does what (I believe) you want it to do ;)

public class GsonClassNameTest {
    static Gson create = new GsonBuilder().registerTypeAdapterFactory(RuntimeClassNameTypeAdapterFactory.of(Object.class)).create();
    public static void main(String[] args) {
        String json = create.toJson(new X());
        System.out.println(json);
    }
    public static class X {
        public String test = "asd";
        public int xyz = 23;
        public Y y_class = new Y();
    }
    public static class Y {
        String yTest = "asd2";

        Z zTest = new Z();
    }
    public static class Z {
        long longVal = 25;
        double doubleTest = 2.4;
    }
}

This now outputs this json for you:

{  
   "class":"google.GsonClassNameTest$X",
   "test":"asd",
   "xyz":23,
   "y_class":{  
      "class":"google.GsonClassNameTest$Y",
      "yTest":"asd2",
      "zTest":{  
         "class":"google.GsonClassNameTest$Z",
         "longVal":25,
         "doubleTest":2.4
      }
   }
}

As you can see, Strings, Longs, integers are correctly created. Each class object recursivley got it's classname as well.

This is a generic approach and should work with everything you create. However, if you decide to take this, do me a favour and write a few unit tests ;) Like I mentioned before, I prototyped this implementation.

Hope that gets me a tick :)

Regards,

Artur


pandaadb's amazing answer wasn't completely working for me since it does not handle arrays/lists and there was a problem with deserialization, so I made a couple of changes:

package org.ctbto.osi.fieldapp.util.gson;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * <p>
*  Disclaimer: taken from here https://stackoverflow.com/a/40133286/285091 with some modifications
 * </p>
 *
 * Adapts values whose runtime type may differ from their declaration type. This
 * is necessary when a field's type is not the same type that GSON should create
 * when deserializing that field. For example, consider these types:
 * <pre>   {@code
 *   abstract class Shape {
 *     int x;
 *     int y;
 *   }
 *   class Circle extends Shape {
 *     int radius;
 *   }
 *   class Rectangle extends Shape {
 *     int width;
 *     int height;
 *   }
 *   class Diamond extends Shape {
 *     int width;
 *     int height;
 *   }
 *   class Drawing {
 *     Shape bottomShape;
 *     Shape topShape;
 *   }
 * }</pre>
 * <p>Without additional type information, the serialized JSON is ambiguous. Is
 * the bottom shape in this drawing a rectangle or a diamond? <pre>   {@code
 *   {
 *     "bottomShape": {
 *       "width": 10,
 *       "height": 5,
 *       "x": 0,
 *       "y": 0
 *     },
 *     "topShape": {
 *       "radius": 2,
 *       "x": 4,
 *       "y": 1
 *     }
 *   }}</pre>
 * This class addresses this problem by adding type information to the
 * serialized JSON and honoring that type information when the JSON is
 * deserialized: <pre>   {@code
 *   {
 *     "bottomShape": {
 *       "type": "Diamond",
 *       "width": 10,
 *       "height": 5,
 *       "x": 0,
 *       "y": 0
 *     },
 *     "topShape": {
 *       "type": "Circle",
 *       "radius": 2,
 *       "x": 4,
 *       "y": 1
 *     }
 *   }}</pre>
 * Both the type field name ({@code "type"}) and the type labels ({@code
 * "Rectangle"}) are configurable.
 * <p>
 * <h3>Registering Types</h3>
 * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
 * name to the {@link #of} factory method. If you don't supply an explicit type
 * field name, {@code "type"} will be used. <pre>   {@code
 *   RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
 *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
 * }</pre>
 * Next register all of your subtypes. Every subtype must be explicitly
 * registered. This protects your application from injection attacks. If you
 * don't supply an explicit type label, the type's simple name will be used.
 * <pre>   {@code
 *   shapeAdapter.registerSubtype(Rectangle.class, "Rectangle");
 *   shapeAdapter.registerSubtype(Circle.class, "Circle");
 *   shapeAdapter.registerSubtype(Diamond.class, "Diamond");
 * }</pre>
 * Finally, register the type adapter factory in your application's GSON builder:
 * <pre>   {@code
 *   Gson gson = new GsonBuilder()
 *       .registerTypeAdapterFactory(shapeAdapterFactory)
 *       .create();
 * }</pre>
 * Like {@code GsonBuilder}, this API supports chaining: <pre>   {@code
 *   RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
 *       .registerSubtype(Rectangle.class)
 *       .registerSubtype(Circle.class)
 *       .registerSubtype(Diamond.class);
 * }</pre>
 */
public final class RuntimeClassNameTypeAdapterFactory<T> implements TypeAdapterFactory {
    private final Class<?> baseType;
    private final String typeFieldName;
    private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<String, Class<?>>();
    private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<Class<?>, String>();

    private RuntimeClassNameTypeAdapterFactory(Class<?> baseType, String typeFieldName) {
        if (typeFieldName == null || baseType == null) {
            throw new NullPointerException();
        }
        this.baseType = baseType;
        this.typeFieldName = typeFieldName;
    }

    /**
     * Creates a new runtime type adapter using for {@code baseType} using {@code
     * typeFieldName} as the type field name. Type field names are case sensitive.
     */
    public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
        return new RuntimeClassNameTypeAdapterFactory<T>(baseType, typeFieldName);
    }

    /**
     * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
     * the type field name.
     */
    public static <T> RuntimeClassNameTypeAdapterFactory<T> of(Class<T> baseType) {
        return new RuntimeClassNameTypeAdapterFactory<T>(baseType, "class");
    }

    /**
     * Registers {@code type} identified by {@code label}. Labels are case
     * sensitive.
     *
     * @throws IllegalArgumentException if either {@code type} or {@code label}
     *                                  have already been registered on this type adapter.
     */
    public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
        if (type == null || label == null) {
            throw new NullPointerException();
        }
        if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
            throw new IllegalArgumentException("types and labels must be unique");
        }
        labelToSubtype.put(label, type);
        subtypeToLabel.put(type, label);
        return this;
    }

    /**
     * Registers {@code type} identified by its {@link Class#getSimpleName simple
     * name}. Labels are case sensitive.
     *
     * @throws IllegalArgumentException if either {@code type} or its simple name
     *                                  have already been registered on this type adapter.
     */
    public RuntimeClassNameTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
        return registerSubtype(type, type.getSimpleName());
    }

    public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {

        final Map<String, TypeAdapter<?>> labelToDelegate
                = new LinkedHashMap<String, TypeAdapter<?>>();
        final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate
                = new LinkedHashMap<Class<?>, TypeAdapter<?>>();

//    && !String.class.isAssignableFrom(type.getRawType())

        if (Object.class.isAssignableFrom(type.getRawType())) {
            TypeAdapter<?> delegate = gson.getDelegateAdapter(this, type);
            labelToDelegate.put(type.getRawType().getName(), delegate);
            subtypeToDelegate.put(type.getRawType(), delegate);
        }

//    for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
//      TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
//      labelToDelegate.put(entry.getKey(), delegate);
//      subtypeToDelegate.put(entry.getValue(), delegate);
//    }

        return new TypeAdapter<R>() {
            @SuppressWarnings("unchecked")
            @Override
            public R read(JsonReader in) throws IOException {
                JsonElement jsonElement = Streams.parse(in);
                if (jsonElement.isJsonObject()) {
                    JsonElement labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
                    if (labelJsonElement == null) {
                        throw new JsonParseException("cannot deserialize " + baseType
                                + " because it does not define a field named " + typeFieldName);
                    }
                    String label = labelJsonElement.getAsString();
                    TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
                    if (delegate == null) {
                        Class<R> aClass;
                        try {
                            aClass = (Class<R>) Class.forName(label);
                        } catch (ClassNotFoundException e) {
                            throw new JsonParseException("Cannot find class " + label, e);
                        }

                        TypeToken<R> subClass = TypeToken.get(aClass);
                        delegate = gson.getDelegateAdapter(RuntimeClassNameTypeAdapterFactory.this, subClass);
                        if (delegate == null) {
                            throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
                                    + label + "; did you forget to register a subtype?");
                        }
                    }
                    return delegate.fromJsonTree(jsonElement);
                } else if (jsonElement.isJsonNull()) {
                    return null;
                } else {
                    TypeAdapter<R> delegate = gson.getDelegateAdapter(RuntimeClassNameTypeAdapterFactory.this, type);
                    if (delegate == null) {
                        throw new JsonParseException("cannot deserialize " + baseType + "; did you forget to register a subtype?");
                    }
                    return delegate.fromJsonTree(jsonElement);
                }
            }

            @Override
            public void write(JsonWriter out, R value) throws IOException {
                Class<?> srcType = value.getClass();
                String label = srcType.getName();
                TypeAdapter<R> delegate = getDelegate(srcType);
                if (delegate == null) {
                    throw new JsonParseException("cannot serialize " + srcType.getName()
                            + "; did you forget to register a subtype?");
                }
                JsonElement jsonTree = delegate.toJsonTree(value);
                if (!jsonTree.isJsonObject()) {
                    Streams.write(jsonTree, out);
                } else {
                    JsonObject jsonObject = jsonTree.getAsJsonObject();
                    if (jsonObject.has(typeFieldName)) {
                        throw new JsonParseException("cannot serialize " + srcType.getName()
                                + " because it already defines a field named " + typeFieldName);
                    }
                    JsonObject clone = new JsonObject();
                    clone.add(typeFieldName, new JsonPrimitive(label));
                    for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
                        clone.add(e.getKey(), e.getValue());
                    }
                    Streams.write(clone, out);
                }
            }

            @SuppressWarnings("unchecked")
            private TypeAdapter<R> getDelegate(Class<?> srcType) {
                TypeAdapter<?> typeAdapter = subtypeToDelegate.get(srcType);
                if (typeAdapter != null) {
                    return (TypeAdapter<R>) typeAdapter;
                }

                for (Map.Entry<Class<?>, TypeAdapter<?>> classTypeAdapterEntry : subtypeToDelegate.entrySet()) {
                    if (classTypeAdapterEntry.getKey().isAssignableFrom(srcType)) {
                        return (TypeAdapter<R>) classTypeAdapterEntry.getValue();
                    }
                }
                return null;
            }
        }.nullSafe();
    }
}

All credit still goes to him/her, though. As s/he says, please test this code before using it!


Accepted @pandaadb's answer but just wanted to paste the code I'm using. It takes care of serializing with type and de-serializing into the proper subtybe:

package com.mycompany.javatest;

import com.google.gson.*;
import java.lang.reflect.*;
import org.junit.*;

public class JavaTest {

    public static class GenericSerializer implements JsonSerializer<Object>, JsonDeserializer<Object> {

        private static final String CLASS_PROPERTY_NAME = "class";
        private final Gson gson;

        public GenericSerializer() {
            gson = new Gson();
        }

        public GenericSerializer(Gson gson) {
            this.gson = gson;
        }

        @Override
        public Object deserialize(JsonElement json, Type typeOfT,
                                  JsonDeserializationContext context) throws JsonParseException {

            Class actualClass;
            if (json.isJsonObject()) {
                JsonObject jsonObject = json.getAsJsonObject();
                String className = jsonObject.get(CLASS_PROPERTY_NAME).getAsString();
                try {
                    actualClass = Class.forName(className);
                }
                catch (ClassNotFoundException e) {
                    e.printStackTrace();
                    throw new JsonParseException(e.getMessage());
                }
            }
            else {
                actualClass = typeOfT.getClass();
            }

            return gson.fromJson(json, actualClass);
        }

        @Override
        public JsonElement serialize(Object src, Type typeOfSrc,
                                     JsonSerializationContext context) {
            JsonElement retValue = gson.toJsonTree(src);
            if (retValue.isJsonObject()) {
                retValue.getAsJsonObject().addProperty(CLASS_PROPERTY_NAME, src.getClass().getName());
            }
            return retValue;
        }

    }

    public static void main(String[] args) {

        GsonBuilder builder = new GsonBuilder();
        builder.registerTypeHierarchyAdapter(Object.class, new GenericSerializer());
        Gson gson = builder.create();

        SomeSuperClass x = new SomeSubClass();
        String json = gson.toJson(x);
        
        SomeSuperClass y = gson.fromJson(json, SomeSuperClass.class); // Usually, y would now be of type SomeSuperClass
        Assert.assertEquals(x.getClass(), y.getClass()); // y is actually of type SomeSubClass (!)
        
        System.out.println("y.getClass()= " + y.getClass());
    }

    public static class SomeSuperClass {
    }

    public static class SomeSubClass extends SomeSuperClass {

        private final String someMember = "12345";
    }
}

Just tried this myself and this seems to work:

public class GsonClassNameTest {


    public static void main(String[] args) {

        Gson create = new GsonBuilder().registerTypeHierarchyAdapter(Object.class, new ODeserialiser()).create();
        String json = create.toJson(new X());
        System.out.println(json);

    }

    public static class ODeserialiser implements JsonSerializer<Object> {

        @Override
        public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContext context) {
            Gson gson = new Gson();
            JsonElement serialize = gson.toJsonTree(src);
            JsonObject o = (JsonObject) serialize;
            o.addProperty("class", src.getClass().getName());
            return serialize;
        }
    }

    public static class X {
        public String test = "asd";
    }
}

This prints:

{"test":"asd","class":"google.GsonClassNameTest$X"}

details:

You have to register a Hierarchy adapter, so that if you register it with the Object class, it will be called for any type you pass into it.

You also have to use a different Gson instance within the custom Serializer, otherwise you just keep running in circles and get a Stackoverflow.

Other than that, pretty straight forward :)

Note: I have rather little experience with gson, so there may be a cooler solution to this.

Regards,

Artur

Tags:

Java

Json

Gson