如何使用 scala 反序列化包含 @@ 的 json 字符串'

how to deserialize a json string that contains @@ with scala'

正如标题已经解释的那样,我想反序列化一个 json 字符串,其中包含一个以 @@ 开头的键。使用@@ 我的标准方法使用 case 类 遗憾地不再起作用。

val test = """{"@@key": "value"}"""
case class Test(@@key: String) // not possible
val gson =  new GsonBuilder().create()
val res = gson.fromJson(test, classOf[Test])

如何在不对输入 json 字符串进行预处理的情况下使用 @@?

最简单的答案是引用字段名称:

case class Test(`@@key`: String)

我做了一些实验,但似乎 GSON 与 Scala case classes 不能很好地互操作(或者相反,我想这是一个角度问题)。我试着玩弄 scala.beans.BeanProperty 但它似乎并没有什么不同。

一种可能的方法是使用常规 classSerializedName 注释,如本例所示:

import com.google.gson.{FieldNamingPolicy, GsonBuilder}
import com.google.gson.annotations.SerializedName

final class Test(k: String) {
  @SerializedName("@@key") val key = k
  override def toString(): String = s"Test($key)"
}

val test = """{"@@key": "foobar"}"""
val gson =  new GsonBuilder().create()
val res = gson.fromJson(test, classOf[Test])
println(res)

您可以尝试使用此代码here on Scastie.

您可以阅读更多关于 SerializedName(以及其他与命名相关的 GSON 功能)here on the user guide

我不是 Scala 程序员,我只是使用 javap 和反射来检查 Scala 编译器生成的内容以及稍微 "learnt" 一些 Scala 内部结构的工作原理。 由于以下几个原因,它对您不起作用:

  • Scala 编译器将 case class 元素注释放在构造函数参数中,而 Gson @SerializedName 只能使用 fields and methods:
// does not work as expected
case class Test(@SerializedName("@@key") `@@key`: String)

从平淡的Java角度来看:

final Constructor<Test> constructor = Test.class.getDeclaredConstructor(String.class);
System.out.println(constructor);
System.out.println(Arrays.deepToString(constructor.getParameterAnnotations()));

public Test(java.lang.String)
[[@com.google.gson.annotations.SerializedName(alternate=[], value=@@key)]]

不确定为什么 Scala 编译器不直接将注解复制到字段中,但是 Java 语言不允许使用 @SerializedName 注解来注解参数导致编译错误(JVM 不允许也将其视为失败)。

  • 字段名称实际上是在 class 文件中编码的。

从Java的角度来看:

final Field field = Test.class.getDeclaredField("$at$atkey"); // the real name of the `@@key` element
System.out.println(field);
System.out.println(Arrays.deepToString(field.getDeclaredAnnotations()));

private final java.lang.String Test.$at$atkey <- this is how the field can be accessed from Java
[] <- no annotations by default

  • Scala 允许将注释移动到字段,这将使您的代码根据 Gson @SerializedName 的设计方式工作(当然,不考虑 Scala):
import scala.annotation.meta.field
...
case class Test(@(SerializedName@field)("@@key") `@@key`: String)

Test(value)


如果由于 some/any 的原因你必须使用 Gson 并且不能用 @SerializedName 注释每个字段,那么你可以实现一个自定义类型适配器,但恐怕你必须深入了解 Scala 的工作原理。 如果我了解 Scala 的作用,它会使用 @ScalaSignature 注释对每个生成的 class 进行注释。 注释提供了 bytes() 方法,该方法 returns 最有可能用于检测注释类型是否为 case class 的有效载荷,以及其成员的声明方式。 我没有找到这样的 parser/decoder,但是如果你找到了,你可以在 Gson 中执行以下操作:

  • 注册一个类型适配器工厂,检查它是否可以处理它(基本上,分析 @ScalaSignature 注释,我相信);
  • 如果可以,则创建一个类型适配器,它可以识别所有大小写 class 字段,它们的名称可能会自己处理 @SerializedName,因为您既不能扩展 Gson ReflectiveTypeAdapterFactory也不注入名称重映射策略;
  • 考虑 transient 字段(永远)和其他排除策略(为了完整性);
  • read/write 每个未排除的字段。

工作量太大了吧?所以我在这里看到两个简单的选择:要么像其他人建议的那样使用 Scala-aware JSON 工具,要么注释具有这种特殊名称的每个字段。