Gson - How to enable a class name property when serializing an object of any type

I came to understand that I need to include the class name as a property when serializing an object in my application. It would be better if I added a class name property for any non-primitive object that is serialized.

I saw that this is a built-in function in Genson using the useClassMetadata method. But I already use gson in my project, so it would be helpful if I could stick with it.

This is my current attempt:

 package com.mycompany.javatest; import com.google.gson.*; import java.lang.reflect.*; public class JavaTest { public static class GenericSerializer implements JsonSerializer<Object>, JsonDeserializer<Object> { private static final String CLASS_PROPERTY_NAME = "class"; @Override public JsonElement serialize(Object src, Type typeOfSrc, JsonSerializationContext context) { JsonElement retValue = context.serialize(src); if (retValue.isJsonObject()) { retValue.getAsJsonObject().addProperty(CLASS_PROPERTY_NAME, src.getClass().getName()); } return retValue; } @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 context.deserialize(json, actualClass); } } public static class MyClass { private final String name = "SpongePants SquareBob"; } public static void main(String[] args) { MyClass obj = new MyClass(); GsonBuilder gb = new GsonBuilder(); gb.registerTypeAdapter(Object.class, new GenericSerializer()); Gson gson = gb.create(); System.out.println(gson.toJson(obj, Object.class)); } } 

Print

 {"name":"SpongePants SquareBob"} 

I want him to print

 {"name":"SpongePants SquareBob","class":"com.mycompany.javatest$MyClass"} 

EDIT: Another try (this time using GsonFire)

 package com.mycompany.javatest; import com.google.gson.*; import io.gsonfire.*; public class JavaTest { public static class DummyData { private final String someData = "1337"; } private static final String CLASS_PROPERTY_NAME = "class"; public static void main(String[] args) { GsonFireBuilder gfb = new GsonFireBuilder(); gfb.registerPostProcessor(Object.class, new PostProcessor<Object>() { @Override public void postDeserialize(Object t, JsonElement je, Gson gson) { // Ignore } @Override public void postSerialize(JsonElement je, Object t, Gson gson) { if (je.isJsonObject()) { je.getAsJsonObject().add(CLASS_PROPERTY_NAME, new JsonPrimitive(t.getClass().getTypeName())); } } }); gfb.registerTypeSelector(Object.class, (JsonElement je) -> { System.out.println(je); if (je.isJsonObject()) { try { return Class.forName(je.getAsJsonObject().get(CLASS_PROPERTY_NAME).getAsString()); } catch (ClassNotFoundException ex) { ex.printStackTrace(); } } return null; }); Gson gson = gfb.createGson(); DummyData dd = new DummyData(); String json = gson.toJson(dd); System.out.println(json); DummyData dd2 = (DummyData) gson.fromJson(json, Object.class); // <-- gives me a ClassCastException } } 
+6
source share
3 answers

Another answer. It took a little longer.

Lateral comment: the above solution will work if you recursively use reflection to develop fields in your class. Then serialize the series with a special serializer using one separate for the parent. This will avoid stackoverflow.

Having said that, I'm a lazy developer, so I like to do something lazy. I am adapting the Google solution for you.

NOTE: PLEASE CHECK THIS AND ADAPT THIS TO YOUR NEEDS. THIS IS A PROTOTYPE AND I DID NOT LOSE THE NECESSARY CODE OR CHECKED FOR POSSIBLE QUESTIONS>

Source code source:

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

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

This, obviously, offers us what we want: recursively register different adapters for class types that can handle them, and not work in circles and call stackoverflow. With one important problem: you need to register ALL subclasses. This is obviously not suitable (although it can be argued that you can solve the problem with the classpath and simply add all your classes at startup once to be able to use it everywhere). So I looked at the source and changed the code to do this dynamically. Please note that Google warns about this - 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 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 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 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 the maven central center, although you can find it here: https://mvnrepository.com/artifact/org.danilopianini/gson-extras/0.1.0

No matter what you need to adapt to make you work for you, I made a copy. The copy is fully compiled, and you can simply paste it into your code and save additional dependency.

The important bits of this code are the following: (and I intentionally left them, but commented out so you can tell)

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

Check if raw can be assigned from String. You want this to apply to every object in the class, so this will take care of that. Pay attention to the code before it looks, if the type is registered in the class, it is no longer needed (accordingly, variables are not needed, you must clear the code)

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

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

String label = srcType.getName();

Secondly, we must distinguish between primitive and object types. Primitive types are strings, integers, etc. In the world of Gson. This means that our check above (adding an adapter) is not captured by the fact that these types of objects are primitive types of grandfathers. So:

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

This will take care of it. If it is primitive, just write the tree to the stream. If this is not the case, we write all the other AND fields in the class field.

 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, it will take care of it. And here is an example to prove that my code does what (I believe) you need 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; } } 

Now you output 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, long, integers are correctly created. Each recursivley class object also received a class name.

This is a general approach and should work with everything you create. However, if you decide to accept this, do me a favor and write some unit tests;) As I mentioned earlier, I prototyped this implementation.

Hope I get a tick :)

Hi,

Arthur

+2
source

Just tried this myself and it 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"; } } 

Fingerprints:

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

more details:

You need to register the hierarchy adapter so that if you register it with the Object class, it will be called for any type that you pass to it.

You also need to use a different instance of Gson in your custom Serializer, otherwise you just keep working in circles and get Stackoverflow.

Other than that, pretty straight forward :)

Note. I have pretty little experience with gson, so there might be a cooler solution for this.

Hi,

Arthur

+2
source

Accepted @padaadb's answer, but just wanted to insert the code I use. It takes care of serialization with type and de-serialization into the corresponding subtype:

 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"; } } 
+1
source

All Articles