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

Cannot deserialize abstract class with type parameter

Open alexblickenstaff opened this issue 6 years ago • 1 comments

I'm trying to serialize and deserialize an abstract class that has a type parameter, but the deserialization isn't working. Instead of deserializing the JSON into an object of the type parameter's class, Jackson deserializes it into a Map, so I'm unable to read the object's properties.

Here is the code:

import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule

object GithubExample {

  @JsonTypeInfo(
    use = JsonTypeInfo.Id.NAME,
    include = JsonTypeInfo.As.PROPERTY,
    property = "@type")
  @JsonSubTypes(Array(
    new JsonSubTypes.Type(value = classOf[ResultWrapperSuccess[_]]),
    new JsonSubTypes.Type(value = classOf[ResultWrapperFailure[_]])
  ))
  trait ResultWrapperInterface[T] {
    protected def obj: T
  }

  case class ResultWrapperSuccess[T](result: T) extends ResultWrapperInterface[T] {
    override protected def obj: T = result
  }

  case class ResultWrapperFailure[F](failure: F) extends ResultWrapperInterface[F] {
    override protected def obj: F = failure
  }

  case class User(name: String, age: Option[Int])

  def main(args: Array[String]): Unit = {

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

    val user = User("John Smith", Some(39))
    val serializedUser = mapper.writeValueAsString(user)
    println(s"(1)        serializedUser: $serializedUser")

    val deserializedUser = mapper.readValue(serializedUser, classOf[User])
    println(s"(2)      deserializedUser: $deserializedUser")
    println(s"(3) deserializedUser.name: ${deserializedUser.name}")


    val wrapperSuccess = ResultWrapperSuccess[User](user)
    val serializedSuccess = mapper.writeValueAsString(wrapperSuccess)
    println(s"(4)     serializedSuccess: $serializedSuccess")

    val deserializedSuccess = mapper.readValue(serializedSuccess, classOf[ResultWrapperInterface[User]])
    deserializedSuccess match {
      case _: ResultWrapperFailure[_] =>
      case success: ResultWrapperSuccess[User] =>
        println(s"(5)               success: $success")
        println(s"(6)        success.result: ${success.result}")
        println(s"(7)   success.result.name: ${success.result.name}")
    }

  }

}

The first part when we serialize and deserialize the User object works just fine. The code breaks on (7) when it tries to access success.result.name because success.result is somehow a Map instead of a User.

Here is the output:

(1)        serializedUser: {"name":"John Smith","age":39}
(2)      deserializedUser: User(John Smith,Some(39))
(3) deserializedUser.name: John Smith
(4)     serializedSuccess: {"@type":"GithubExample$ResultWrapperSuccess","result":{"name":"John Smith","age":39}}
(5)               success: ResultWrapperSuccess(Map(name -> John Smith, age -> 39))
(6)        success.result: Map(name -> John Smith, age -> 39)
Exception in thread "main" java.lang.ClassCastException: scala.collection.immutable.Map$Map2 cannot be cast to GithubExample$User
    at GithubExample$.main(GithubExample.scala:55)
    at GithubExample.main(GithubExample.scala)

As evidenced by the logs, the serialization seems to be working just fine. Is there something I need to change to get the deserialization working?

alexblickenstaff avatar Oct 22 '19 20:10 alexblickenstaff

Combination of generic type and polymorphic type handling is not supported by jackson-databind (both are supported separately, just not in this combination), so you would need to refactor things to achieve polymorphism separately for wrapper (to get success/fail subtypes), and then for enclosed value type (using @JsonTypeInfo on result / failure property).

cowtowncoder avatar Oct 22 '19 20:10 cowtowncoder