如何反序列化 Java/Scala 中的 Json 字符串并继承 class 并持有不同的属性

How to deserialize Json string in Java/Scala with inherit class holding different attributes

我正在尝试在 scala 中反序列化以下字符串

{ "nodeid": 30, "depth": 6, "split": 65, "split_condition": -9.53674316e-07, "yes": 43, "no": 44, "missing": 43 , "children": [
  { "nodeid": 43, "leaf": 0.000833201397 },
  { "nodeid": 44, "leaf": -0.00145038881 }
]}

它是一个树结构,我们可以认为有非叶子节点和叶子节点,这两者都扩展了一个抽象节点类型。他们拥有不同的属性。 (这个字符串是从 XGBoost 转储的,我想通过反序列化它来创建我自己的数据结构)

我尝试使用 Jackson,但它只需要一种 class 类型。如果我定义一个包含所有这些属性的 class,它肯定可以工作,但之后我无法转储为相同的格式。

那么除了自定义覆盖deserialize,还有其他更好的选择吗?

如果答不出问题,可以参考我下面的例子

我希望将字符串反序列化为具有以下定义的 Node class

class Node {
  var nodeid: Int = 0
}
class NotLeaf extends Node {
  var depth: Int = 0
  var split: Int = 0
  ...
  var children: List[Node] = null
}
class Leaf extends Node {
  var leaf: Float = 0
}

对于我试过的代码,我可以定义 class

class Node {
  var nodeid: Int = 0
  var depth: Int = 0
  var split: Int = 0
  var split_condition: Float = 0
  var yes: Int = 0
  var no: Int = 0
  var missing: Int = 0
  var children: List[XGBoostFormat] = null
  var leaf: Float = 0
}

并且反序列化结果将具有所有属性,如果我序列化回来,它将是

{
  "nodeid" : 30,
  "depth" : 6,
  "split" : 65,
  "split_condition" : -9.536743E-7,
  "yes" : 43,
  "no" : 44,
  "missing" : 43,
  "children" : [ {
    "nodeid" : "43",
    "depth" : 0,
    "split" : 0,
    "split_condition" : 0.0,
    "yes" : 0,
    "no" : 0,
    "missing" : 0,
    "children" : null,
    "leaf" : 8.332014E-4
  }, {
    "nodeid" : "44",
    "depth" : 0,
    "split" : 0,
    "split_condition" : 0.0,
    "yes" : 0,
    "no" : 0,
    "missing" : 0,
    "children" : null,
    "leaf" : -0.0014503888
  } ],
  "leaf" : 0.0
}

在 Scala 中,最好使用基于编译时反射而不是 运行时间反射的 Scala JSON 库。它们可以自动生成编解码器——只要您坚持 Scala 使用 sealed traits(或 sealed abstract classes)和 case classes 建模数据的方式。你也不应该使用 vars 和 nulls.

可以解析您的数据的库有(除其他外):Circe、Play JSON、Jsoniter Scala、Json4s。其中前两个可以这样使用:

import cats.syntax.functor._ // for Decoder widen
import io.circe.Decoder
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto._
import io.circe.parser

import play.api.libs.json._

sealed trait Tree extends Product with Serializable {
  val nodeid: Int
}
object Tree {
  
  // Circe config for derivation
  private implicit val circeConfig: Configuration =
    Configuration.default.withSnakeCaseMemberNames
  
  // PlayJson config for derivation
  private implicit val playJsonConfig: JsonConfiguration =
    JsonConfiguration(JsonNaming.SnakeCase)
  
  final case class Node(
    nodeid: Int,
    depth: Int,
    split: Int,
    splitCondition: Float,
    yes: Int,
    no: Int,
    children: List[Tree]
  ) extends Tree
  object Node {
    
    implicit def nodeCirceDecoder: Decoder[Node] =
      deriveConfiguredDecoder
    
    implicit def nodePlayJsonDecoder: Reads[Node] =
      Json.reads[Node]
  }
  
  final case class Leaf(
    nodeid: Int,
    leaf: Float
  ) extends Tree
  object Leaf {
    
    implicit val leafCirceDecoder: Decoder[Leaf] =
      deriveConfiguredDecoder
    
    implicit val leafPlayJsonDecoder: Reads[Leaf] =
      Json.reads[Leaf]
  }

  // combined manually because no discriminator field
  
  implicit val treeCirceDecoder: Decoder[Tree] =
    Decoder[Leaf].widen[Tree] or Decoder[Node].widen[Tree]
  
  // normally I'd use orElse on Reads but it's not by-name (lazy)
  implicit val treePlayJsonDecoder: Reads[Tree] = Reads[Tree] { json =>
    implicitly[Reads[Leaf]].widen[Tree].reads(json) orElse implicitly[Reads[Node]].widen[Tree].reads(json)
  }
}

val input = """{ "nodeid": 30, "depth": 6, "split": 65, "split_condition": -9.53674316e-07, "yes": 43, "no": 44, "missing": 43 , "children": [
              |  { "nodeid": 43, "leaf": 0.000833201397 },
              |  { "nodeid": 44, "leaf": -0.00145038881 }
              |]}""".stripMargin

io.circe.parser.decode[Tree](input)

Json.fromJson[Tree](Json.parse(input))

(scastie example)

  • 使用宏来派生编解码器(你的情况是特定的,因为你有带子类型的递归数据结构并且没有区分字段 - 有区分字段就不需要手动组合编解码器)
  • 将它们放入伴随对象中,这样您就不必将它们导入范围
  • 运行 解析代码 returns Either[SomeError, YourType]

(示例使用两个库来演示如何使用它们,但您只需要选择一个)。

由于对象是一次性构建或完全构建的,因此没有理由使其字段可变并用空值初始化。

我还建议使用 Doubles,或者更好的 BigDecimals,而不是 Floats,如果你使用这样精确的值。

如果你想用 Java 方式,使用空值、变量、注释和 运行时间反射 (Jackson),那么最好用 Java 问这个问题标记为 Scala 开发人员积极避免这些。