jackson-module-scala icon indicating copy to clipboard operation
jackson-module-scala copied to clipboard

Cannot deserialize a Map whose key is a case class?

Open ms-ati opened this issue 9 years ago • 7 comments

Are Maps with keys that are simple case classes (of one primitive field) supported?

import com.fasterxml.jackson.databind._
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper

val mapper = (new ObjectMapper() with ScalaObjectMapper).
        registerModule(DefaultScalaModule).
        configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).
        findAndRegisterModules(). // register joda and java-time modules automatically
        asInstanceOf[ObjectMapper with ScalaObjectMapper]

case class Foo(n: Int)

val m: Map[Foo, String] = Map(Foo(1) -> "bar")

val s = mapper.writeValueAsString(m)
// s: String = {"Foo(1)":"bar"}

mapper.readValue[Map[Foo, String]](s)
// com.fasterxml.jackson.databind.JsonMappingException: Can not find a (Map) Key deserializer for type [simple type, class Foo]
// at [Source: {"Foo(1)":"bar"}; line: 1, column: 1]
// at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:244)
// at com.fasterxml.jackson.databind.deser.DeserializerCache._handleUnknownKeyDeserializer(DeserializerCache.java:587)
// at com.fasterxml.jackson.databind.deser.DeserializerCache.findKeyDeserializer(DeserializerCache.java:168)
// at com.fasterxml.jackson.databind.DeserializationContext.findKeyDeserializer(DeserializationContext.java:500)
// at com.fasterxml.jackson.module.scala.deser.UnsortedMapDeserializer$$anonfun$1.apply(UnsortedMapDeserializerModule.scala:70)
// at com.fasterxml.jackson.module.scala.deser.UnsortedMapDeserializer$$anonfun$1.apply(UnsortedMapDeserializerModule.scala:70)
// at scala.Option.getOrElse(Option.scala:121)
// at com.fasterxml.jackson.module.scala.deser.UnsortedMapDeserializer.createContextual(UnsortedMapDeserializerModule.scala:70)
// at com.fasterxml.jackson.module.scala.deser.UnsortedMapDeserializer.createContextual(UnsortedMapDeserializerModule.scala:39)
// at com.fasterxml.jackson.databind.DeserializationContext.handleSecondaryContextualization(DeserializationContext.java:685)
// at com.fasterxml.jackson.databind.DeserializationContext.findRootValueDeserializer(DeserializationContext.java:482)
// at com.fasterxml.jackson.databind.ObjectMapper._findRootDeserializer(ObjectMapper.java:3890)
// at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3785)
// at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2817)
// at com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper$class.readValue(ScalaObjectMapper.scala:184)
// at $anon$1.readValue(<console>:17)
// ... 42 elided

ms-ati avatar Apr 12 '16 19:04 ms-ati

Unfortunately, json does not support objects as keys. What is happening is that Foo(1) is being stringified as "Foo(1)" (the same as calling .toString on the object) before writing it as the json key. It is not possible to deserialize "Foo(1)" as an instance of Foo of 1.

I can imagine that a value class (i.e. a case class that extends AnyVal) of an integer or a string would be a desirable key, but currently the module does not support this behavior.

Does that make sense?

nbauernfeind avatar Apr 14 '16 02:04 nbauernfeind

Ah, that does make sense @nbauernfeind, thank you. In this case I guess the best approach is to have an intermediate data structure then, for (de-)serialization, which uses the unwrapped values for keys?

ms-tg avatar Apr 14 '16 12:04 ms-tg

It really depends on what you are trying to do, but I find myself doing something like this instead:

val keyMap = Map[String, KeyType]()
val valMap = Map[String, ValType]()

Or if you really are using value classes, then write your own accessors/modifiers that strip out the id from the value class.

nbauernfeind avatar Apr 14 '16 16:04 nbauernfeind

We are using value classes. But there's enough limitations with Jackson and Scala that I found it easier just to have transformations to/from simplified representations for Jackson.

ms-tg avatar Apr 14 '16 17:04 ms-tg

If I'm remembering correctly, it is not possible to tell the difference between value classes and primitives without using scala reflection. However scala 2.10's reflection is not thread safe which makes it a non-starter. I don't think we will be able to support them without carving a 2.10 branch, which won't support the new scala things, or waiting until we stop supporting 2.10.

nbauernfeind avatar Apr 14 '16 18:04 nbauernfeind

Not sure if this helps, but it is possible to add custom key serializers, deserializers, either for types, or for Map-valued properties. Other custom serializers, deserializers are only used for values; for deserialization there is separate KeyDeserializer, for serialization JsonSerializer is used, but it has to use different write method. So this is why separate ones for keys are needed.

Default set of handlers is quite small (most primitives, enums, Dates), and more extensive for one direction (serialization I think).

cowtowncoder avatar Apr 14 '16 18:04 cowtowncoder

Yeah I get it. Once 2.10 is not supported, a lot more stuff can be done in this module. As it stands, I contend that transforming to a simplified representation is a lot easier than getting the various custom KeyDeserializer and JsonSerializers all correct...

ms-tg avatar Apr 14 '16 18:04 ms-tg