Search code examples
scalagenericscastingjacksonmatch

Invalid when I convert a data type in a generic function. toint is called, but the type of v is still double


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)

Solution

  • 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
      }