I want to implement a method to convert JSON data into Scala built-in objects. The code is similar to the following:
implicit class JsonNodeToScala(jsonNode: JsonNode) {
import scala.jdk.CollectionConverters._
import scala.reflect.{ClassTag,classTag}
def toSeq[T: ClassTag]: Seq[Option[T]] = jsonNode.elements.asScala.map(_.toScala[T]).toSeq
def toMap[T: ClassTag]: Map[String, Option[T]] = jsonNode.fields.asScala.map(field => (field.getKey, field.getValue.toScala[T])).toMap
def toScala[T: ClassTag]: Option[T] = (this.jsonNode match {
case v if v.isBoolean => v.asBoolean
case v if v.isNumber =>
(v.asDouble, classTag.runtimeClass) match {
case (v, c) if c == classOf[Int] =>
print("toInt")
v.toInt
case (v, c) if c == classOf[Long] =>
print("toLong")
v.toLong
case (v, _) => v
}
case v if v.isTextual => v.asText
case jsonNode => jsonNode
}) match {
case v: T => Some(v)
case v =>
println(v, classTag.runtimeClass, v.getClass)
None
}
}
It works when I use it as follows
new ObjectMapper().readTree("""[1,2,null,4]""").toSeq[Double]
>>> res136: Seq[Option[Double]] = List(
Some(value = 1.0),
Some(value = 2.0),
None,
Some(value = 4.0)
)
But when I pass in the generic parameter int, I can't get the result I want.
new ObjectMapper().readTree("""[1,2,null,4]""").toSeq[Int]
>>>
toInt(1.0,int,class java.lang.Double)
toInt(2.0,int,class java.lang.Double)
(null,int,class com.fasterxml.jackson.databind.node.NullNode)
toInt(4.0,int,class java.lang.Double)
res138: Seq[Option[Int]] = List(None, None, None, None)
As a result, toint is called, but the type of v is still double. Is it caused by Scala's processing in match case?
and if I want to meet my needs, how should I modify my code?
"[1, 2, 3]" call toSeq[Int] => Seq(1, 2, 3)
"[1, 2, 3]" call toSeq[Double] => Seq(1.0, 2.0, 3.0)
"[1.0, 2.0, 3.0]" call toSeq[Int] => Seq(1, 2, 3)
"[1.0, 2.0, 3.0]" call toSeq[Double] => Seq(1.0, 2.0, 3.0)
The main problem here is that in:
case v if v.isNumber =>
(v.asDouble, classTag.runtimeClass) match {
case (v, c) if c == classOf[Int] =>
print("toInt")
v.toInt
case (v, c) if c == classOf[Long] =>
print("toLong")
v.toLong
case (v, _) => v
}
The Scala compiler has to find the common lowest ancestor between Int
, Long
and Double
.
In Scala 2.12, it happens to be Double
, so it cast Int
to Double
again (via implicit conversion).
A way to trick the compiler should be to upcast some return type so it cannot perform an implicit type conversion between numerical types. For instance:
case v if v.isNumber =>
(v.asDouble, classTag.runtimeClass) match {
case (v, c) if c == classOf[Int] =>
print("toInt")
v.toInt
case (v, c) if c == classOf[Long] =>
print("toLong")
v.toLong
case (v, _) => (v : Any) // common lowest type now is Any, no conversions will be performed
}