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.
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?
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.
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.)
Related
I'm using Jackson in my Scala/Spark program and I've distilled my issue to the simple example
below. My problem is that when my case class has the Option[Int] field (age) set to None
I see reasonable deserialization output (that is: a struct with empty=true). However, when
age is defined, i.e., set to some Int like Some(99), I never see the integer value in the
deserialization output .
Given :
import com.fasterxml.jackson.databind.ObjectMapper
import java.io.ByteArrayOutputStream
import scala.beans.BeanProperty
case class Dog(#BeanProperty name: String, #BeanProperty age: Option[Integer])
object OtherTest extends App {
jsonOut(Dog("rex", None))
jsonOut(Dog("mex", Some(99)))
private def jsonOut(dog: Dog) = {
val mapper = new ObjectMapper();
val stream = new ByteArrayOutputStream();
mapper.writeValue(stream, dog);
System.out.println("result:" + stream.toString());
}
}
My output is as shown below. Any hints/help greatly appreciated !
result:{"name":"rex","age":{"empty":true,"defined":false}}
result:{"name":"mex","age":{"empty":false,"defined":true}}
Update after the Helpful Answer
Here are the dependencies that worked for me:
implementation 'org.scala-lang:scala-library:2.12.2'
implementation "org.apache.spark:spark-sql_2.12:3.1.2"
implementation "org.apache.spark:spark-sql-kafka-0-10_2.12:3.1.2"
implementation "org.apache.spark:spark-avro_2.12:3.1.2"
implementation 'com.fasterxml.jackson.module:jackson-module-scala_2.12:2.10.0'
Here is the updated code (with frequent flyer bonus - round trip example):
private def jsonOut(dog: Dog) = {
val mapper = new ObjectMapper()
mapper.registerModule(DefaultScalaModule)
val stream = new ByteArrayOutputStream();
mapper.writeValue(stream, dog);
val serialized = stream.toString()
System.out.println("result:" + serialized);
// verify we can read the serialized thing back to case class:
val recovered = mapper.readValue(serialized, classOf[Dog])
System.out.println("here is what we read back:" + recovered);
}
Here is the resultant output (as expected now ;^) ->
> Task :OtherTest.main()
result:{"name":"rex","age":null}
here is what we read back:Dog(rex,None)
result:{"name":"mex","age":99}
here is what we read back:Dog(mex,Some(99))
You need to add the Jackson module for Scala to make it work with standard Scala data types.
Add this module as your dependency: https://github.com/FasterXML/jackson-module-scala
Follow the readme on how to initialize your ObjectMapper with this module.
I want to load & parse a JSON file with scala 2.11.8, in a generic way, like this:
private val objectMapper = new ObjectMapper with ScalaObjectMapper
objectMapper.registerModule(DefaultScalaModule)
def loadFile[T](path: Path): Try[T] = Try(
objectMapper.readValue(Files.readAllBytes(path), classOf[T])
)
Then the goal is to call the loadFile method with only the expected return type.
However this returns me:
class type required but T found
By googling, I found references to erasures, manifests, ClassTag but nothing works. What is the correct solution?
The generic type gets erased, so you need a ClassTag to make it work. This is how you can use them:
def loadFile[T: ClassTag](path: Path): Try[T] = Try(
objectMapper.readValue(
Files.readAllBytes(path),
implicitly[ClassTag[T]].runtimeClass.asInstanceOf[Class[T]])
)
(For some reason, runtimeClass doesn't have the generic type, so you need the cast.)
This is what I got:
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModuleobject
AppStart extends App {
val mapper = new ObjectMapper()
mapper.registerModule(DefaultScalaModule)
val json = """{"id":"AB","stuff":"whatever"}"""
val obj = mapper.readValue(json, classOf[TestClass])
println(obj.id.get) // prints AB !!!
}
case class TestClass(id: Option[Int] = None, stuff: Option[String] = None)
At the same time, this does not even build:
val bad: Option[Int] = "AB"
There is clearly something wrong here. Versions that I am using in project:
scalaVersion := "2.11.6"
libraryDependencies += "com.fasterxml.jackson.module" % "jackson-module-scala_2.11" % "2.7.3"
No, this doesn't break JVM type safety. JVM doesn't support generics, so far as it's concerned the type of id is Option, not Option[Int]. To break type safety, you'd have to get a TestClass whose id is not an Option.
Reflection and casting certainly break Java's and Scala's type safety, and they are supposed to.
To deserialize generics properly in Jackson, you need to supply additional information (see http://wiki.fasterxml.com/JacksonFAQ#Deserializing_Generic_types) and jackson-module-scala allows Scala compiler to supply it:
You can also mixin ScalaObjectMapper (experimental) to get rich wrappers that automatically convert scala manifests directly into TypeReferences for Jackson to use:
val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
val myMap = mapper.readValue[Map[String,Tuple2[Int,Int]]](src)
I don't know if it'll automatically prevent the issue you have as well.
You would need to look at the byte code of the class Scala generates.
My guess is that reflection is still working, however the runtime type of the field is not what it appears in Scala, so the JVM doesn't prevent this happening.
I am newbie to play framework.
I had seen chapter 'JSON' in play framework documentation
It guide me to use case class, not normal class
so It seemed not to support json to class mapping
is it true?
"sorry for poor expression skill, I'm not a native"
What is exactly your problem ? Here is how it works in Java, it must be very close in Scala.
Let's say you have a class MyClass and and instance myObject of this class.
If you want to serialize :
JsonNode json = play.libs.Json.toJson(myObject);
And if you want to deserialize :
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
MyClass myObject = mapper.readValue(jsonNode.toString(), MyClass.class);
Obviously, you'll need to handle JsonParseException, JsonMappingException... to send a human readable message to your end user.
I seem to be able to use Jackson to make a mapper of Json-String --> scala.collection.Map.
How can I hook up that same mapper to a RestTemplate?
val restTemplate = new RestTemplate()
val module = new OptionModule with MapModule with SeqModule with IteratorModule
val mapper = new ObjectMapper()
mapper.registerModule(module)
// Get some example JSON
val uri = "http://...."
val response:String = restTemplate.getForObject(uri, classOf[String] )
// *** success ***
// Use the mapper directly: String --> scala.collection.Map
val map1 = mapper.readValue(response, classOf[scala.collection.Map[String, Any]])
// Try hooking up the same module to the RestTemplate:
val wrappingConverter = new WrappingHttpMessageConverter()
wrappingConverter.getObjectMapper().registerModule(module)
val list = restTemplate.getMessageConverters()
list.add(wrappingConverter)
restTemplate.setMessageConverters(list)
// *** FAILS ***
// org.springframework.http.converter.HttpMessageNotReadableException: Could not read
// JSON: Can not construct instance of scala.collection.Map, problem: abstract types
// either need to be mapped to concrete types, have custom deserializer, or be
// instantiated with additional type information
val map2 = restTemplate.getForObject(uri, classOf[scala.collection.Map[String, Any]] )
Assumtions
WrappingHttpMessageConverter is a derived class of MappingJackson2HttpMessageConverter, or some class that works like it.
You're using Spring 4.0 (though this answer is probably also true for Spring 3.2)
The problem
The default RestTemplate constructor tries to detect if Jackson is on your classpath, and if it is, adds MappingJackson2HttpMessageConverter to the MessageConverters list. Since it's already on the list, it's going to be used before your WrappingHttpMessageConverter is ever checked.
That default converter doesn't have the Scala module installed. Here's where things get tricky. HttpMessageConverterExtractor tries to ask if the first converter can deserialize the type; currently ObjectMapper returns true for this test (whether it should is a much longer topic, not as clear cut as it might seem). The extractor doesn't handle the idea that one converter could fail, but a later one might succeed (as it would in your case).
Workarounds
You need to make sure that Spring finds an ObjectMapper configured with the Scala module before it tries with any other. You can do this in a number of ways; the most robust is to search the preconfigured converters and update the first you find, adding a new one if you don't find any:
val jacksonConverter = list.asScala.collectFirst {
case p: MappingJackson2HttpMessageConverter => p
}
if (jacksonConverter.isDefined) {
jacksonConverter.get.getObjectMapper.registerModule(module)
}
else {
list.add(new MappingJackson2HttpMessageConverter) // or your derived class if you prefer
}
Other options include adding your custom message converter to the front of the list, or removing the existing Jackson converter before adding your own.