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

Collection of case classes deserialized as Collection of Map2

Open jameskyle opened this issue 1 year ago • 4 comments

import com.fasterxml.jackson.databind.{ObjectMapper, SerializationFeature}
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.databind.json.{JsonMapper}

case class B(a: String, b: String)

val mapper = JsonMapper.builder().addModule(DefaultScalaModule).build()

mapper.writeValueAsString(B("a", "b"))
// "{\"a\":\"a\",\"b\":\"b\"}"

val s = mapper.writeValueAsString(List(B("a", "b"), B("c", "d")))
// "[{\"a\":\"a\",\"b\":\"b\"},{\"a\":\"c\",\"b\":\"d\"}]"

mapper.readValue(s, classOf[List[B]])
// List(Map("a" -> "a", "b" -> "b"), Map("a" -> "c", "b" -> "d"))

Any attempt to use the values throws a cast error. Unable to cast Map2 to B.

jameskyle avatar Oct 09 '23 22:10 jameskyle

Try this:

    val mapper = JsonMapper.builder().addModule(DefaultScalaModule).build() :: ClassTagExtensions
    val data = List(B("a", "b"), B("c", "d"))
    val s = mapper.writeValueAsString(data)
    mapper.readValue[List[B]](s) shouldEqual data

Jackson is a Java library based on Java Reflection. This approach is problematic due to Type Erasure.

ClassTagExtensions uses Scala ClassTags to preserve more of the type information.

pjfanning avatar Oct 09 '23 23:10 pjfanning

This also seems to work:

    val mapper = JsonMapper.builder().addModule(DefaultScalaModule).build()
    val data = List(B("a", "b"), B("c", "d"))
    val s = mapper.writeValueAsString(data)
    mapper.readValue(s, new TypeReference[List[B]] {}) shouldEqual data

The TypeReference is another way of preserving type information that would otherwise by lost to Type Erasure.

pjfanning avatar Oct 09 '23 23:10 pjfanning

Great! I assumed it was a type erasure issue.

I had some issues trying ClassYagExtension that were unrelated to the immediate problem (probably build environment related).

I'll give the Type Reference approach a try.

jameskyle avatar Oct 10 '23 12:10 jameskyle

Try this:

    val mapper = JsonMapper.builder().addModule(DefaultScalaModule).build() :: ClassTagExtensions
    val data = List(B("a", "b"), B("c", "d"))
    val s = mapper.writeValueAsString(data)
    mapper.readValue[List[B]](s) shouldEqual data

Jackson is a Java library based on Java Reflection. This approach is problematic due to Type Erasure.

ClassTagExtensions uses Scala ClassTags to preserve more of the type information.

Just reporting back, this doesn't actually pass

  it should "encode and decode consistently using class tag extension" in {
    case class B(a: String, b: String)
    val mapper = JsonMapper.builder().addModule(DefaultScalaModule).build() :: ClassTagExtensions
    val data = List(B("a", "b"), B("c", "d"))
    val s = mapper.writeValueAsString(data)
    mapper.readValue[List[B]](s) shouldEqual data
  }

Produces the following failed test

List(Map("a" -> "a", "b" -> "b"), Map("a" -> "c", "b" -> "d")) did not equal List(B(a,b), B(c,d))
ScalaTestFailureLocation: com.safegraph.galaxy.maps.elastic_fusion.entities.self_serve.SelfServeDataGenerationConfigTest at (SelfServeDataGenerationConfigTest.scala:24)
Expected :List(B(a,b), B(c,d))
Actual   :List(Map("a" -> "a", "b" -> "b"), Map("a" -> "c", "b" -> "d"))

But this one works..

This also seems to work:

val mapper = JsonMapper.builder().addModule(DefaultScalaModule).build() val data = List(B("a", "b"), B("c", "d")) val s = mapper.writeValueAsString(data) mapper.readValue(s, new TypeReference[List[B]] {}) shouldEqual data The TypeReference is another way of preserving type information that would > otherwise by lost to Type Erasure.

Any idea why the first doesn't? I ran the example in a repl and it did seem to work. But not as a unit test.

jameskyle avatar Oct 10 '23 23:10 jameskyle