I'm trying to implement "generic" deserialization in JSON for simple Scala3 types (e.g. case classes) - I need to pass in the serialized JSON and the name (string) of the target class and to receive an instance of the target class (which I treat as a java.lang.Object
afterwards).
I chose to use the Lift framework's JSON library. Due to Lift being Scala2 only, I'm using it through the CrossVersion.for3Use2_13 compatibility layer (this is from my build.sbt):
libraryDependencies += ("net.liftweb" %% "lift-json" % "3.5.0").cross(CrossVersion.for3Use2_13),
I chose Lift's JSON library due to the availability of explicitly passing the target class for deserialization (see Extraction Lift docs):
def extract(json: JValue, target: TypeInfo)(implicit formats: Formats): Any
To make use of that, I'm first "dereferencing" the class name to a java.lang.Class instance in Java:
String targetClassName = (inp.entrypointClass == null ? "Main" : inp.entrypointClass);
String mainClassName = this.getClass().getPackage().getName() + "." + targetClassName;
Class targetClass;
try {
targetClass = Class.forName(mainClassName);
} catch(ClassNotFoundException err1) {
throw new InvalidEntrypointClassError(err1);
}
This part seems to run correctly (evidenced by the fact that I'm not getting InvalidEntrypointClassError
s in my debugging attempts).
Then I try to deserialize the string into a Class as follows:
import net.liftweb.json._
import net.liftweb.json.Extraction._
import net.liftweb.json.Serialization._
import reflect._
val formats = net.liftweb.json.DefaultFormats
object ScalaWrapper {
def deserialize(jsonData: java.lang.String, clazz: java.lang.Class[?]): java.lang.Object = {
val typeInfo = TypeInfo(clazz, None)
val data = parse(jsonData)
val res: Any = extract(data, typeInfo)(formats)
res.asInstanceOf[java.lang.Object]
}
}
This fails when I try it with simple examples, such as trying to deserialize into the Input
case class defined here:
case class Node(name: String, left: Option[Int], right: Option[Int])
case class Walk(nodes: List[Node])
case class Input(nodes: List[Node])
Note that it is important for me to have "simple" case class definitions, that is passing the complexity of the deserialization problem unto the case classes IS NOT a solution due to other requirements
with the following JSON input:
{
"nodes": [
{
"name": "A",
"left": 1,
"right": 2
},
{
"name": "B"
},
{
"name": "C",
"left": 4,
"right": 3
},
{
"name": "D"
},
{
"name": "E"
}
]
}
The error is rather deep inside Lift JSON:
java.lang.NullPointerException
at scala.tools.scalap.scalax.rules.scalasig.ByteCode$.forClass(ClassFileParser.scala:24)
at scala.tools.scalap.scalax.rules.scalasig.ScalaSigParser$.parse(ScalaSig.scala:68)
at net.liftweb.json.ScalaSigReader$.findScalaSig(ScalaSig.scala:126)
at net.liftweb.json.ScalaSigReader$.$anonfun$findScalaSig$1(ScalaSig.scala:126)
at scala.Option.orElse(Option.scala:477)
at net.liftweb.json.ScalaSigReader$.findScalaSig(ScalaSig.scala:126)
at net.liftweb.json.ScalaSigReader$.findClass(ScalaSig.scala:60)
at net.liftweb.json.ScalaSigReader$.readConstructor(ScalaSig.scala:44)
... <SKIPPED ABOUT 20 OTHER STACK FRAMES>
at net.liftweb.json.Meta$.mappingOf(Meta.scala:195)
at net.liftweb.json.Extraction$.extract(Extraction.scala:210)
at binary_tree_dfs.ScalaWrapper$.deserialize(ScalaWrapper.scala:13)
at binary_tree_dfs.ScalaWrapper.deserialize(ScalaWrapper.scala)
I've failed at debugging this and I don't know Scala under the hood to really understand what's going on, but my guess is I'm paying the price for trying to use Scala2 generics features in a Scala3 project. Is that correct? Can this code be fixed without a major refactor?
Since I don't know what's going on here, I've thought about several other approaches:
izumi-reflect
's TypeTag
s into Scala2 Manifest
-compatible objects, but I don't know how to do it and I'm afraid this is hacky and unreliable. Is this approach possible and does it make more sense?TypeTag
that is provided as (implicit?) argument. Is there a Scala3 library that would allow me to do this without modifying the case classes definitions?java.lang.Class
and Manifest
? Does that even make sense if Manifest doesn't have a generic argument?I know this question is a lot to take in, so thanks for making it this far :) I've tried hard to provide as much possible info without overcomplicating, but I'd gladly share more context - such as a not-working example that I'm sweating over
Just to summarize the problem:
I'm trying to deserialize a JSON string into a Scala3 class I know only by name.
I expect this class to be a case class. I'm currently trying and failing to use Scala2's Lift JSON library.
I want the case classes that the JSON will be deserialized into to have simple definitions, so I don't want serialize
/deserialize
methods or similar approaches.
How do I achieve this in Scala 3?
Jackson (with the Scala module) is able to do what you want (i.e. parsing to a class known at runtime).
package com.myapp
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.scala.{ClassTagExtensions, DefaultScalaModule}
case class Node(name: String, left: Option[Int], right: Option[Int])
case class Walk(nodes: List[Node])
case class Input(nodes: List[Node])
val objectMapper = JsonMapper.builder().addModule(DefaultScalaModule).build() :: ClassTagExtensions
val input: String = ???
val parsedInput = objectMapper.readValue(input, Class.forName("com.myapp.Input"))
println(parsedInput)
// Input(List(Node(A,Some(1),Some(2)), Node(B,None,None), Node(C,Some(4),Some(3)), Node(D,None,None), Node(E,None,None)))