0

I have a Scala Map keyed by a type that itself needs serialising to JSON. Because of the nature of JSON that requires key names for objects to be strings, a simple mapping is not directly possible.

The work around I wish to implement is to convert the Map to a Set before serialising to JSON, and then from a Set back to a Map after deserialising.

I am aware of other methods using key serialisers on specific types, e.g. Serialising Map with Jackson, however, I require a solution that applies to arbitrary key types and in this regard, conversion to Set and back again looks to me like the best option.

I've had some success serialising to a Set with a wrapper object by modifying MapSerializerModule.scala from jackson-module-scala below, but I'm not familiar enough with the Jackson internals to get that JSON to deserialise back into the Map I started with.

I should add that I control both the serialisation and deserialisation side, so what the JSON looks like is not significant.

case class Wrapper[K, V](
  value: Set[(K, V)]
)

class MapConverter[K, V](inputType: JavaType, config: SerializationConfig)
  extends StdConverter[Map[K, V], Wrapper[K, V]] {
  def convert(value: Map[K, V]): Wrapper[K, V] = {
    val set = value.toSet
    Wrapper(set)
  }

  override def getInputType(factory: TypeFactory) = inputType

  override def getOutputType(factory: TypeFactory) =
    factory.constructReferenceType(classOf[Wrapper[_, _]], inputType.getContentType)
      .withTypeHandler(inputType.getTypeHandler)
      .withValueHandler(inputType.getValueHandler)
}

object MapSerializerResolver extends Serializers.Base {

  val MAP = classOf[Map[_, _]]

  override def findMapLikeSerializer(
    config: SerializationConfig,
    typ: MapLikeType,
    beanDesc: BeanDescription,
    keySerializer: JsonSerializer[AnyRef],
    elementTypeSerializer: TypeSerializer,
    elementValueSerializer: JsonSerializer[AnyRef]): JsonSerializer[_] = {

    val rawClass = typ.getRawClass

    if (!MAP.isAssignableFrom(rawClass)) null
    else new StdDelegatingSerializer(new MapConverter(typ, config))
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    val objMap = Map(
      new Key("k1", "k2") -> "k1k2",
      new Key("k2", "k3") -> "k2k3")

    val om = new ObjectMapper()
    om.registerModule(DefaultScalaModule)
    om.registerModule(ConverterModule)

    val res = om.writeValueAsString(objMap)
    println(res)
  }
}
oal
  • 411
  • 5
  • 15
  • I give you an answer which is a no answer: annotations and runtime reflection are the source of all evil. Leave that world and move towards typeclasses based approach – Edmondo Feb 19 '17 at 18:48

2 Answers2

0

I managed to find the solution:

case class MapWrapper[K, V](
  wrappedMap: Set[MapEntry[K, V]]
)

case class MapEntry[K, V](
  key: K,
  value: V
)

object MapConverter extends SimpleModule {
  addSerializer(classOf[Map[_, _]], new StdDelegatingSerializer(new StdConverter[Map[_, _], MapWrapper[_, _]] {
    def convert(inMap: Map[_, _]): MapWrapper[_, _] = MapWrapper(inMap map { case (k, v) => MapEntry(k, v) } toSet)
  }))

  addDeserializer(classOf[Map[_, _]], new StdDelegatingDeserializer(new StdConverter[MapWrapper[_, _], Map[_, _]] {
    def convert(mapWrapper: MapWrapper[_, _]): Map[_, _] = mapWrapper.wrappedMap map { case MapEntry(k, v) => (k, v) } toMap
  }))
}

class MapKey(
  val k1: String,
  val k2: String
) {
  override def toString: String = s"MapKey($k1, $k2) (str)"
}


object Main {
  def main(args: Array[String]): Unit = {
    val objMap = Map(
      new MapKey("k1", "k2") -> "k1k2",
      new MapKey("k2", "k3") -> "k2k3")

    val om = setupObjectMapper

    val jsonMap = om.writeValueAsString(objMap)

    val deserMap = om.readValue(jsonMap, classOf[Map[_, _]])
  }

  private def setupObjectMapper = {
    val typeResolverBuilder =
      new DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.NON_FINAL) {
        init(JsonTypeInfo.Id.CLASS, null)
        inclusion(JsonTypeInfo.As.WRAPPER_OBJECT)
        typeProperty("@CLASS")

        override def useForType(t: JavaType): Boolean = !t.isContainerType && super.useForType(t)
      }

    val om = new ObjectMapper()
    om.registerModule(DefaultScalaModule)
    om.registerModule(MapConverter)
    om.setDefaultTyping(typeResolverBuilder)

    om
  }
}

Interestingly, if the key type is a case class, the MapConverter is not necessary since the case class can be reconstituted from the string representation.

oal
  • 411
  • 5
  • 15
0

In my case, I had a nested Map. This required a small addition to the conversion back into a map:

addDeserializer(classOf[Map[_, _]], new StdDelegatingDeserializer(new StdConverter[MapWrapper[_, _], Map[_, _]] {
  def convert(mapWrapper: MapWrapper[_, _]): Map[_, _] = {
    mapWrapper.wrappedMap.map { case MapEntry(k, v) => {
      v match {
        case wm: MapWrapper[_, _] => (k, convert(wm))
        case _ => (k, v)
      }
    }}.toMap
  }
}))
Wouter
  • 1