1
votes

I'm trying to serialize/deserialize a Scala class to JSON using Jackson ObjectMapper. The serialization works fine, but I was getting type exceptions trying to read the JSON back in. I fixed most of those by adding appropriate annotations, but it's not working for my Map members... it seems like Jackson is trying to treat the keys in the JSON object as properties in a class instead of keys in a map. (I believe this is different than other questions like this one since they are calling readValue on the map contents directly.)

Here's my ObjectMapper setup:

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

Here's what my annotated class and member look like:

@JsonInclude(JsonInclude.Include.NON_DEFAULT)
class MyClass extends Serializable {
  @JsonDeserialize(
    as = classOf[mutable.HashMap[String, Long]],
    keyAs = classOf[java.lang.String],
    contentAs = classOf[java.lang.Long]
  )
  val counts = mutable.Map.empty[String, Long]
}

If I give it some JSON like:

{"counts":{"foo":1,"bar":2}}

And read it with mapper.readValue[MyClass](jsonString)

I get an exception like UnrecognizedPropertyException: Unrecognized field "foo" (class mutable.HashMap), not marked as ignorable.

I tried adding DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES to my mapper configuration but that didn't seem to do anything in this case, and I'm not sure that kind of global setting is desirable.

How do I convince Jackson to treat the strings "foo" and "bar" as keys in the map member field and not as properties in the HashMap class? It seems to have done the right thing automatically writing it out.

Also worth noting: the deserialization appears to work fine in a quick out/in unit test to a temp file or a string variable, but not when I try to run the whole application and it reads the JSON its previously written. I don't know why it seems to work in the test, as far as I know it's making the same readValue call.

3

3 Answers

0
votes

I made one simple test like this:

case class TestClass (counts: mutable.HashMap[String, Long])

And I converted it like:

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

val r3 = objectMapper.readValue("{\"counts\":{\"foo\":1,\"bar\":2}}", classOf[TestClass])

And apparently it works for me. Maybe it's something about the version you're using of Jackson, or Scala. Have you tried different versions of Jackson for example?

-1
votes

Try jsoniter-scala and you will enjoy how it can be handy, safely, and efficient to parse and serialize JSON these days with Scala: https://github.com/plokhotnyuk/jsoniter-scala

One of it's crucial features is an ability to generate codecs in compile time and even print their sources. You will have no any runtime magic like reflection or byte code replacement that will affect your code.

-1
votes

My problem was a race condition due to not using chaining in my mapper configuration singleton.

My old code was more like this:

private var mapper: ObjectMapper with ScalaObjectMapper = _

def getMapper: ObjectMapper with ScalaObjectMapper = {
  if (mapper == null) {
    mapper = new ObjectMapper() with ScalaObjectMapper
    mapper.registerModule(DefaultScalaModule)
    mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
  }
  mapper
}

As you can see, if one thread initializes the mapper, but hasn't yet disabled unknown properties failure, a second thread could return and use a mapper that hasn't had that flag set yet, which explains why I was seeing the error only some of the time.

The correct code uses chaining so that the mapper singleton is set with all of the configuration:

private var mapper: ObjectMapper = _

 def getMapper: ObjectMapper = {
   if (mapper == null) {
     mapper = new ObjectMapper()
       .registerModule(DefaultScalaModule)
       .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
   }
   mapper
 }

(I also removed the experimental ScalaObjectMapper mix-in.)