问题
As the title already explains, I would like to deserialize a json string that contains a key that starts with @@. With the @@ my standard approach using case classes sadly does not work anymore.
val test = """{"@@key": "value"}"""
case class Test(@@key: String) // not possible
val gson = new GsonBuilder().create()
val res = gson.fromJson(test, classOf[Test])
How can work with the @@ withtout preprocessing the input json string?
回答1:
The simplest answer is to quote the field name:
case class Test(`@@key`: String)
回答2:
I experimented a bit but it seems that GSON doesn't interoperate well with Scala case class
es (or the other way around, I guess it's a matter of perspective). I tried playing around with scala.beans.BeanProperty
but it doesn't seem like it makes a difference.
A possible way to go is to use a regular class
and the SerializedName
annotation, as in this example:
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)
You can play around with this code here on Scastie.
You can read more on SerializedName
(as well as other naming-related GSON features) here on the user guide.
回答3:
I'm not a Scala programmer, I just used javap
and reflection to check what the Scala compiler generated and slightly "learnt" how some Scala internals work.
It does not work for you because of several reasons:
- The Scala compiler puts case class elements annotations to the constructor parameters, whereas Gson
@SerializedName
can only work with fields and methods:
// does not work as expected
case class Test(@SerializedName("@@key") `@@key`: String)
From the plain Java perspective:
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)]]
Not sure why the Scala compiler does not replicate the annotations directly to the fields, but the Java language does not allow annotating parameters with the @SerializedName
annotation causing a compilation error (JVM does not treats it as a failure either).
- The field name is actually encoded in the class file.
From the Java perspective:
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 allows moving annotations to fields and this would make your code work accordingly to how Gson
@SerializedName
is designed (of course, no Scala in mind):
import scala.annotation.meta.field
...
case class Test(@(SerializedName@field)("@@key") `@@key`: String)
Test(value)
If for some/any reason you must use Gson and can't annotate each field with @SerializedName
, then you can implement a custom type adapter, but I'm afraid that you have to have deep knowledge in how Scala works.
If I understand what Scala does, it annotates every generated class with the @ScalaSignature
annotation.
The annotation provides the bytes()
method that returns a payload that's most likely can be used to detect whether the annotated type is a case class, and probably how its members are declared.
I didn't find such a parser/decoder, but if you find one, you can do the following in Gson:
- register a type adapter factory that checks whether it can handle it (basically, analyzing the
@ScalaSignature
annotation, I believe); - if it can, then create a type adapter that is aware of all case class fields, their names possibly handling the
@SerializedName
yourself, as you can neither extend GsonReflectiveTypeAdapterFactory
nor inject a name remapping strategy; - take
transient
fields (for good) and other exclusion strategies (for completeness) into account; - read/write each non-excluded field.
Too much work, right? So I see two easy options here: either use a Scala-aware JSON tool like other people are suggesting, or annotate each field that have such a special name.
来源:https://stackoverflow.com/questions/62259410/how-to-deserialize-a-json-string-that-contains-with-scala