Moje zdjęcie
Software Craftsman's Blog by Marcin Pieciukiewicz
Java and Scala development

Monday, April 16, 2012

Scala, gson and Option

During development of my pet project written in Scala I've run into a problem when trying to serialize Option object into Json using gson library. This is problem, because Gson have implemented serializers for many Java objects and primitives but it can't handle Option object properly.

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 )
    }
  }
}

1 comment: