Solution
The solution is to impelment your own OptionSerializer and register it during creation of Gson instance:
class OptionSerializer extends JsonSerializer[Option[Any]] with JsonDeserializer[Option[Any]] { def serialize(src: Option[Any], typeOfSrc: Type, context: JsonSerializationContext): JsonElement def deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Option[Any] }
val gsonBuilder = new GsonBuilder gsonBuilder.registerTypeAdapter(classOf[Option[Any]], new OptionSerializer) val gson = gsonBuilder.create
Lets start with implementing serialize method.
First will check if given src is None value. If it is None, then serialize method will return an empty JsonObject. It is important to use empty JsonObject instead of JsonNull, because during deserialization gson will skip value if it is null. And we would like it to set the None value, so we wouldn't end with null.
If the Option is not empty we need to serialize its content using context.serialize method. So our serialize method should look like that:
def serialize(src: Option[Any], typeOfSrc: Type, context: JsonSerializationContext): JsonElement = { if (src.isDefined) { context.serialize(src.get.value) } else { new JsonObject } }
There is a problem with interoperability between Java and Scala, when using parametrized types (generics) with primitives. Simply it is impossible in Java (so the code Option<int> won't compile), but it is legal to write Option[Int] in Scala.
That results in a problem that Option of simple types (Int, Long, Boolean etc.) looses information about underlying type during runtime (passed in ParameterizedType), causing that we will have Option[Object]. And we need to know type of object contained by Option during deserialization.
The solution is to save type information inside json. It isn't very elegant solution, but it works very well.
So the final serialize method will look like that:
def serialize(src: Option[Any], typeOfSrc: Type, context: JsonSerializationContext): JsonElement = { val jsonObject = new JsonObject if (src.isDefined) { def value = src.get jsonObject.addProperty("class", value.asInstanceOf[Object].getClass.getName) jsonObject.add("value", context.serialize(value)) } jsonObject }
When the problems during serialization are resolved, it is quite easy to write deserialization method. We only need to check if the given jsonElement is not empty, and in that case return None. Otherwise we create Option object containing deserialized value.
def deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext):Option[Any] = { if (json.isJsonNull) { None } else if (json.isJsonObject && json.getAsJsonObject.entrySet().size()==0) { None } else { val className = json.getAsJsonObject.get("class").getAsString val deserialized = context.deserialize(json.getAsJsonObject.get("value"), Class.forName(className)) Option(deserialized ) } }
Below is the example of json created from simple object:
Class:
class OptionalDataObject(var intOption:Option[Int], stringOption:Option[String])
Json:
{ "intOption":{"class":"java.lang.Integer","value":3}, "stringOption":{"class":"java.lang.String","value":"test"} }
Result:
And in the end the whole OptionSerializer class:
class OptionSerializer extends JsonSerializer[Option[Any]] with JsonDeserializer[Option[Any]] { def serialize(src: Option[Any], typeOfSrc: Type, context: JsonSerializationContext): JsonElement = { val jsonObject = new JsonObject if (src.isDefined) { def value = src.get jsonObject.addProperty("class", value.asInstanceOf[Object].getClass.getName) jsonObject.add("value", context.serialize(value)) } jsonObject } def deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext):Option[Any] = { if (json.isJsonNull) { None } else if (json.isJsonObject && json.getAsJsonObject.entrySet().size()==0) { None } else { val className = json.getAsJsonObject.get("class").getAsString val deserialized = context.deserialize(json.getAsJsonObject.get("value"), Class.forName(className)) Option(deserialized ) } } }