FasterXML / jackson-module-scala

Add-on module for Jackson (https://github.com/FasterXML/jackson) to support Scala-specific datatypes
Apache License 2.0
501 stars 141 forks source link

Cannot deserialize a Map whose key is a case class? #251

Open ms-ati opened 8 years ago

ms-ati commented 8 years ago

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
nbauernfeind commented 8 years ago

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?

ms-tg commented 8 years ago

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?

nbauernfeind commented 8 years ago

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.

ms-tg commented 8 years ago

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.

nbauernfeind commented 8 years ago

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.

cowtowncoder commented 8 years ago

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

ms-tg commented 8 years ago

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...