FasterXML / jackson-module-scala

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

Parameterized type violated when deserializing an Option #243

Open salokanto opened 8 years ago

salokanto commented 8 years ago

I can deserialize just about anything into an Option:

case class Foo(integer: Option[Int])

object OptionFail {
  val mapper = new ObjectMapper() with ScalaObjectMapper
  mapper.registerModule(DefaultScalaModule)

  def main(args: Array[String]) {
    val json = """{"integer" : "a cat"}"""
    println(mapper.readValue[Foo](json))
    // prints: Foo(Some(a cat))
  }
}

Using:

libraryDependencies ++= Seq(
  "com.fasterxml.jackson.core" % "jackson-databind" % "2.7.3",
  "com.fasterxml.jackson.module" % "jackson-module-scala_2.11" % "2.7.2"
)
chasebradford commented 8 years ago

1) ObjectMapper is a java class with a generic readValue call. It doesn't have any runtime type information, so you have to give it some by passing the class. mapper.readValue(json, classOf[Foo]) 2) Passing classOf or a TypeReference is usually good enough to get deep generic type info. Unfortunately, generics with primitives suffer from the way scala encodes those types. See: https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-and-other-primitive-challenges

kralikba commented 8 years ago

Unfortunately this latter is not a solution for "deeper" types, e.g. Option[Seq[Int]]. Jackson believes that it is not a compatible type (that being a collection-like type of a collection-like type of a simple type). I've not yet had time to dig deeper, but I suspect that it is a nontrivial fix.

chasebradford commented 8 years ago

I doubt there is a fix for the nested container types. The JVM sees that as Option[Seq[Object]]. Scala just doesn't provide enough type information to figure that out, and the JsonDeserialize option only works for one layer down.

nbauernfeind commented 8 years ago

@kralikba I'm not sure I understand what you mean by "deeper" types. I would expect Option[Seq[T]] to deserialize (and serialize) correctly. Int as T might have issues as @chasebradford points out that generic types using primitives suffer from how scala encodes them (due to boxing on your behalf). Can you provide an example of what you want to do that jackson is having trouble with?

kralikba commented 8 years ago

@nbauernfeind E.g. the following example:

  val mapper = new ObjectMapper with ScalaObjectMapper
  mapper.registerModule(DefaultScalaModule)

  case class Y(os: Option[Seq[Int]])
  println(mapper.readValue[Y]("{}"))

  case class X(@JsonDeserialize(contentAs = classOf[java.lang.Integer]) os: Option[Seq[Int]])
  println(mapper.readValue[X]("{}"))

Outputs Y(None), then fails with the following exception:

Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: 
Failed to narrow value type of [collection-like type; class scala.Option, contains [collection-like type; class scala.collection.Seq, contains [simple type, class java.lang.Object]]] 
with concrete-type annotation (value java.lang.Integer), from '': 
Class java.lang.Integer not subtype of [collection-like type; class scala.collection.Seq, contains [simple type, class java.lang.Object]]
nbauernfeind commented 8 years ago

Thanks for your example. Class X is an invalid use of contentAs since you're declaring the content of the Option to be an Int, not the content of the Seq to be an int. (I'm sure you already know this as you're trying to say that it is not a work around.)

I've verified, as @chasebradford says, that the JVM/Jackson is only able to detect the Option as an Option[Seq[Object]] (instead of Option[Seq[Int]]). Similarly I tried with just Option[Int] or Seq[Int] with the same results. The byte codes makes this look like it is a java.lang.Object.

The code does indeed throw the expected exception if you directly use a TypeReference via mapper.readValue[Option[Int]]("\"a cat\"").

I believe that this can be improved by using Scala reflection in scala 2.11+, and I would like to do this during/for Jackson 2.8.x. Until then, however, I do not think I can make this any better. I will keep it at the top of my mind though. Maybe @cowtowncoder has some ideas that we're not yet taking advantage of.

Your [suboptimal] work arounds:

pjfanning commented 1 year ago

Workarounds documented in https://github.com/FasterXML/jackson-module-scala/wiki/FAQ#deserializing-optionint-seqint-and-other-primitive-challenges