I am quite new to Kotlin but and I have successfully used Kotlin serialization on many cases - works out of the box even for nested classes, mutableLists etc. However I struggle with two dimensional arrays.
My class:
import kotlinx.serialization.*
#Serializable
data class Thing(val name:String)
#Serializable
data class Array2D(val width:Int, val height:Int,
var arrayContents:Array<Array<Thing?>> = Array(1){Array(1){null} }){
init{
arrayContents = Array(width){Array(height){null} }
}
}
And when doing this:
val a = Array2D(2, 2)
a.arrayContents[0][0] = Thing("T0")
a.arrayContents[0][1] = Thing("T1")
a.arrayContents[1][0] = Thing("T2")
a.arrayContents[1][1] = Thing("T3")
val json = Json {
allowStructuredMapKeys = true
}
val jsonString = json.encodeToString(Array2D.serializer(), a)
assertEquals(
"""
{"width":2,"height":2,"arrayContents":[[{"name":"T0"},{"name":"T1"}],[{"name":"T2"},{"name":"T3"}]]}
""".trim(),
jsonString
) // encoding is OK
val b = json.decodeFromString(deserializer = Array2D.serializer(), jsonString)
// this fails to reproduce "a" and stops at first array level
// b.arrayContents = {Thing[2][]} (array of nulls) instead of {Thing[2][2]} (array of array of Thing)
If it can encode the class to String it should decode it as well, right? Or am I missing something here? Maybe I should use custom serializer but there are not many examples that fit my case. One example is https://github.com/Kotlin/kotlinx.serialization/issues/357 but it is only one level of array.
Thanks for any help :)
Serialization/deserialization for arrays (including multi-dimensional) works out of the box.
Unexpected behavior is related to init section of your data class. It's executed after deserialization happens and overwrites data parsed from JSON.
It also happens when you create instance of Array2D manually:
val x = Array2D(1, 1, Array(1) { Array(1) { Thing("0") } })
println(x.arrayContents[0][0]) //will print null
You just need to replace init block with secondary constructor (default value for arrayContents is redundant, by the way), and you may declare arrayContents as val now:
#Serializable
data class Array2D(val width: Int, val height: Int, val arrayContents: Array<Array<Thing?>>) {
constructor(width: Int, height: Int) : this(width, height, Array(width) { Array(height) { null } })
}
Related
i am trying to write code to mask nested json fields..
def maskRecursively(map :mutable.Map[String,Object]):mutable.Map[String,Object] ={
val maskColumns = PII_Data.getPIIData()
for((k,v) <- map){
if(v.isInstanceOf[Map[String,Object]]){
maskRecursively(map.get(k).asInstanceOf[mutable.Map[String,Object]])
}else if(v.isInstanceOf[List[Object]]) {
} else {
if(maskColumns.contains(k)){map+=(k->"*****")}
}
}
map }
calling this method from ..
val mapper = new ObjectMapper()
mapper.registerModule(DefaultScalaModule)
val result = mapper.readValue(jsonStr, classOf[ java.util.Map[String,Object] ])
import scala.collection.JavaConverters._
val myScalaMap = result.asScala
maskRecursively(result.asScala)
i am getting error while trying to iterate a nested json object ..
Cannot cast value of type 'scala.Some' to type 'scala.collection.mutable.Map'
how do i recurse a complex nested json object this way ?
Your mistake was
if(v.isInstanceOf[Map[String,Object]]){
maskRecursively(map.get(k).asInstanceOf[mutable.Map[String,Object]])
There are a few issues:
You check if v is an instance of Map, but then attempt to cast it to mutable.Map. They are technically different types (mutable vs immutable).
You check the type of v, but then apply the cast to map.get(k), which is going to be a different value and type from v. A map's get method returns an Option, hence the error message.
Thanks to type erasure on the JVM, the runtime won't be able to tell the difference between e.g. a Map[String, Object] and a Map[SomethingElse, Whatever] - both will just look like Map at runtime. The compiler should have given you a warning about the isInstanceOf call for this reason.
If you do an isInstanceOf / asInstanceOf combo, make sure the operand is the same each time. You already have v, so you don't need to look it up a second time from the map. And make sure you use the same type on both instanceOf calls.
Fix this by changing it to
if(v.isInstanceOf[mutable.Map[_, _]]){
maskRecursively(v.asInstanceOf[mutable.Map[String,Object]])
After some digging , i was able to solve this..
def maskJson(jsonStr: String): String = {
implicit val formats = org.json4s.DefaultFormats
val mapper = new ObjectMapper()
mapper.registerModule(DefaultScalaModule)
val result = mapper.readValue(jsonStr, classOf[Map[String, Object]])
val maskedJson = maskRecursively(result)
mapper.writeValueAsString(maskedJson)
}
def maskRecursively(map: Map[String, Object]): collection.mutable.Map[String, Object] = {
val mutable = collection.mutable.Map[String, Object]()
val maskColumns = PII_Data.getJsonPIIFields()
for ((k, v) <- map) {
if (v.isInstanceOf[Map[String, Object]]) {
mutable += k -> maskRecursively(v.asInstanceOf[Map[String, Object]])
} else if (v.isInstanceOf[List[Object]]) {
val list = v.asInstanceOf[List[Map[String, Object]]].map(i => maskRecursively(i)).toList
mutable += k -> list
} else {
if (maskColumns.contains(k)) {
mutable += (k -> "*****")
}
else {
mutable += k -> v
}
}
}
mutable
}
TL;DR
For a json string containing ...,field=,..., Gson keeps throwing JsonSyntaxException. What can I do?
The Case
I have to communicate with a 3rd api, Which tends to provide data like this:
{
"fieldA": "stringData",
"fieldB": "",
"fieldC": ""
}
However, In my app project, it turns out to read like this:
val jsonString = "{fieldA=stringData,fieldB=,fieldC=}"
The Problem
I tried using the standard method to deserialize it:
val jsonString = "{fieldA=stringData,fieldB=,fieldC=}"
val parseJson = Gson().fromJson(jsonString, JsonObject::class.java)
assertEquals(3, parseJson.size())
But it results in a Exception:
com.google.gson.JsonSyntaxException: com.google.gson.stream.MalformedJsonException: Unexpected value at line 1 column 28 path $.fieldB
The Solutions That Don't Work
I have tried so many solutions, none of them works. Including:
Setup a custom data class and set value to nullable
data class DataExample(
val fieldA: String?,
val fieldB: String?,
val fieldC: String?,
)
val parseToObject = Gson().fromJson(jsonString, DataExample::class.java)
Using JsonElement instead:
data class DataExample(
val fieldA: JsonElement,
val fieldB: JsonElement,
val fieldC: JsonElement,
)
val parseToObject = Gson().fromJson(jsonString, DataExample::class.java)
Applying a Deserializer:
class EmptyToNullDeserializer<T>: JsonDeserializer<T> {
override fun deserialize(
json: JsonElement, typeOfT: Type, context: JsonDeserializationContext
): T? {
if (json.isJsonPrimitive) {
json.asJsonPrimitive.also {
if (it.isString && it.asString.isEmpty()) return null
}
}
return context.deserialize(json, typeOfT)
}
}
data class DataExample(
#JsonAdapter(EmptyToNullDeserializer::class)
val fieldA: String?,
#JsonAdapter(EmptyToNullDeserializer::class)
val fieldB: String?,
#JsonAdapter(EmptyToNullDeserializer::class)
val fieldC: String?,
)
val parseToObject = Gson().fromJson(jsonString, DataExample::class.java)
or using it in GsonBuilder:
val gson = GsonBuilder()
.registerTypeAdapter(DataExample::class.java, EmptyToNullDeserializer<String>())
.create()
val parseToObject = gson.fromJson(jsonString, DataExample::class.java)
What else can I do?
It is not a valid JSON. You need to parse it by yourself. Probably this string is made by using Map::toString() method.
Here is the code to parse it into Map<String, String>
val jsonString = "{fieldA=stringData,fieldB=,fieldC=}"
val userFieldsMap = jsonString.removeSurrounding("{", "}").split(",") // split by ","
.mapNotNull { fieldString ->
val keyVal = fieldString.split("=")
// check if array contains exactly 2 items
if (keyVal.size == 2) {
keyVal[0].trim() to keyVal[1].trim() // return#mapNotNull
} else {
null // return#mapNotNull
}
}
.toMap()
It turns out that, like #frc129 and many others said, it is not an valid JSON.
The truth is however, Gson handles more situation than JSON should be, like the data below:
val jsonString = "{fieldA=stringData,fieldB=s2,fieldC=s3}"
val parseJson = Gson().fromJson(jsonString, JsonObject::class.java)
// This will NOT throw exception, even the jsonString here is not actually a JSON string.
assertEquals(3, parseJson.size())
assertEquals("stringData", parseJson["fieldA"].asString)
assertEquals("s2", parseJson["fieldB"].asString)
assertEquals("s3", parseJson["fieldC"].asString)
Further investigation indicates that -- the string mentioned here and in the question -- is more like a Map to string.
I got a bit misunderstanding with GSON dealing with Map. That should be treat as a extra handy support, but not a legal procedure. In short, it is not supposed to be transformed, and data format should be fixed. I'll go work with server and base transformation then.
Just leave a note here. If someone in the future want some quick fix to string, you may take a look at #frc129 answer; however, the ideal solution to this is to fix the data provider to provide "the correct JSON format":
val jsonString = "{\"fieldA\":\"stringData\",\"fieldB\":\"\",\"fieldC\":\"\"}"
val parseJson = Gson().fromJson(jsonString, JsonObject::class.java)
assertEquals(3, parseJson.size())
assertEquals("stringData", parseJson["fieldA"].asString)
assertEquals("", parseJson["fieldB"].asString)
assertEquals("", parseJson["fieldC"].asString)
The problem I am trying to solve is perfectly described by the following text got from this link:
For a concrete example of when this could be useful, consider an API that supports partial updates of objects. Using this API, a JSON object would be used to communicate a patch for some long-lived object. Any included property specifies that the corresponding value of the object should be updated, while the values for any omitted properties should remain unchanged. If any of the object’s properties are nullable, then a value of null being sent for a property is fundamentally different than a property that is missing, so these cases must be distinguished.
That post presents a solution but using the kotlinx.serialization library, however, I must use gson library for now.
So I am trying to implement my own solution as I didn't find anything that could suit my use case (please let me know if there is).
data class MyObject(
val fieldOne: OptionalProperty<String> = OptionalProperty.NotPresent,
val fieldTwo: OptionalProperty<String?> = OptionalProperty.NotPresent,
val fieldThree: OptionalProperty<Int> = OptionalProperty.NotPresent
)
fun main() {
val gson = GsonBuilder()
.registerTypeHierarchyAdapter(OptionalProperty::class.java, OptionalPropertyDeserializer())
.create()
val json1 = """{
"fieldOne": "some string",
"fieldTwo": "another string",
"fieldThree": 18
}
"""
println("json1 result object: ${gson.fromJson(json1, MyObject::class.java)}")
val json2 = """{
"fieldOne": "some string",
"fieldThree": 18
}
"""
println("json2 result object: ${gson.fromJson(json2, MyObject::class.java)}")
val json3 = """{
"fieldOne": "some string",
"fieldTwo": null,
"fieldThree": 18
}
"""
println("json3 result object: ${gson.fromJson(json3, MyObject::class.java)}")
}
sealed class OptionalProperty<out T> {
object NotPresent : OptionalProperty<Nothing>()
data class Present<T>(val value: T) : OptionalProperty<T>()
}
class OptionalPropertyDeserializer : JsonDeserializer<OptionalProperty<*>> {
private val gson: Gson = Gson()
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): OptionalProperty<*> {
println("Inside OptionalPropertyDeserializer.deserialize json:$json")
return when {
// Is it a JsonObject? Bingo!
json?.isJsonObject == true ||
json?.isJsonPrimitive == true-> {
// Let's try to extract the type in order
// to deserialize this object
val parameterizedType = typeOfT as ParameterizedType
// Returns an Present with the value deserialized
return OptionalProperty.Present(
context?.deserialize<Any>(
json,
parameterizedType.actualTypeArguments[0]
)!!
)
}
// Wow, is it an array of objects?
json?.isJsonArray == true -> {
// First, let's try to get the array type
val parameterizedType = typeOfT as ParameterizedType
// check if the array contains a generic type too,
// for example, List<Result<T, E>>
if (parameterizedType.actualTypeArguments[0] is WildcardType) {
// In case of yes, let's try to get the type from the
// wildcard type (*)
val internalListType = (parameterizedType.actualTypeArguments[0] as WildcardType).upperBounds[0] as ParameterizedType
// Deserialize the array with the base type Any
// It will give us an array full of linkedTreeMaps (the json)
val arr = context?.deserialize<Any>(json, parameterizedType.actualTypeArguments[0]) as ArrayList<*>
// Iterate the array and
// this time, try to deserialize each member with the discovered
// wildcard type and create new array with these values
val result = arr.map { linkedTreeMap ->
val jsonElement = gson.toJsonTree(linkedTreeMap as LinkedTreeMap<*, *>).asJsonObject
return#map context.deserialize<Any>(jsonElement, internalListType.actualTypeArguments[0])
}
// Return the result inside the Ok state
return OptionalProperty.Present(result)
} else {
// Fortunately it is a simple list, like Array<String>
// Just get the type as with a JsonObject and return an Ok
return OptionalProperty.Present(
context?.deserialize<Any>(
json,
parameterizedType.actualTypeArguments[0]
)!!
)
}
}
// It is not a JsonObject or JsonArray
// Let's returns the default state NotPresent.
else -> OptionalProperty.NotPresent
}
}
}
I got most of the code for the custom deserializer from here.
This is the output when I run the main function:
Inside OptionalPropertyDeserializer.deserialize json:"some string"
Inside OptionalPropertyDeserializer.deserialize json:"another string"
Inside OptionalPropertyDeserializer.deserialize json:18
json1 result object: MyObject(fieldOne=Present(value=some string), fieldTwo=Present(value=another string), fieldThree=Present(value=18))
Inside OptionalPropertyDeserializer.deserialize json:"some string"
Inside OptionalPropertyDeserializer.deserialize json:18
json2 result object: MyObject(fieldOne=Present(value=some string), fieldTwo=my.package.OptionalProperty$NotPresent#573fd745, fieldThree=Present(value=18))
Inside OptionalPropertyDeserializer.deserialize json:"some string"
Inside OptionalPropertyDeserializer.deserialize json:18
json3 result object: MyObject(fieldOne=Present(value=some string), fieldTwo=null, fieldThree=Present(value=18))
I am testing the different options for the fieldTwo and it is almost fully working, with the exception of the 3rd json, where I would expect that fieldTwo should be fieldTwo=Present(value=null) instead of fieldTwo=null.
And I see that in this situation, the custom deserializer is not even called for fieldTwo.
Can anyone spot what I am missing here? Any tip would be very appreciated!
I ended giving up of gson and move to moshi.
I implemented this behavior based on the solution presented in this comment.
I am writing a custom Serializer for kotlin.serialization which interprets a JSON variable as a String no matter of its type. Deserializing works out fine. But I have trouble serializing the String into its original structure again.
At the moment I just deserialize the value as a String. But that adds extra quotes around the value and leads to problems.
Here is a quick example:
//
//Custom serializer:
//
object JsonToStringSerializer : KSerializer<String> {
override val descriptor: SerialDescriptor = PrimitiveDescriptor("JsonToStringSerializer", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: String) {
if(value.startsWith("[") || value.startsWith("{")) {
encoder.encodeString(value) //<-- bad solution
println("Oh no! Object has been turned into a String!")
}
else //will also encode Int, Float, Boolean, ... into String - but that is ok
encoder.encodeString(value)
}
override fun deserialize(decoder: Decoder): String {
val output = (decoder as JsonInput).decodeJson().toString()
return if(output.startsWith("\""))
output.substring(1, output.length-1)
else output
}
}
//
//Data structure for test:
//
#Serializable
class Test (
#Serializable(with = JsonToStringSerializer::class) val objValue: String,
val stringValue: String
)
//
//test logic:
//
fun test() {
val jsonString = "{\"objValue\": [1,2], \"stringValue\": \"test\"}"
val obj = Json(JsonConfiguration.Stable).parse(Test.serializer(), json)
val newJsonString = Json(JsonConfiguration.Stable).stringify(Test.serializer(), obj)
println(newJsonString)
//expected result: {"objValue": [1,2], "stringValue": "test"}
//actual result: {"objValue": "[1,2]", "stringValue": "test"}
}
After some digging I found that encoder in serialize() belongs to the class StreamingJsonOutput.
I can think of three possible solutions. But I ran into a wall with each of them:
If I had a way to directly print() to composer from StreamingJsonOutput I could add the String manually to the output. But unfortunately composer is private (and StreamingJsonOutput is internal as well).
Another way would be to use the configuration unquotedPrint which would create Strings without quotes. But then stringValue would not have quotes either. So I need a way to just change the configuration only temporariy.
I could also just deserialize the value String from serialize() into a real object and then just let the encoder serialize it correctly. The problem, apart from the performance overhead, is that I don't know which datastructure to expect in value. So I am not able to deserialize it.
I hope somebody has an idea how to deal with that.
Thank you very much! :)
I have some history data, that I want to convert to json. So these are Lists of lists. Their type is List[List[Position]] where Position is a simple case class. I wrote a formatter to help Json.toJson cope. I was expecting an output of exactly one outer array with two inner arrays that contain 3 objects each. What I got instead was this. Note the additional array wrappings.
[[[[{"amount":1.0,"minAmount":2.0,"price":3.0,"volume":4.0},
{"amount":5.0,"minAmount":6.0,"price":7.0,"volume":8.0},
{"amount":9.0,"minAmount":10.0,"price":11.0,"volume":12.0}]],
[[{"amount":0.1,"minAmount":0.2,"price":0.3,"volume":0.4},
{"amount":5.0,"minAmount":6.0,"price":7.0,"volume":8.0},
{"amount":9.0,"minAmount":10.0,"price":11.0,"volume":12.0}]]]]
I don't know where the wrapping arrays come from. Can somebody help me out here? This is a test with the wrapper I am using:
class ApplicationSpec extends Specification {
implicit object PositionFormat extends Format[List[List[Position]]] {
def writes(historyList: List[List[Position]]) : JsValue = {
Json.arr(historyList.map{
o => Json.arr(o.map{ p =>
Json.obj(
"amount" -> JsNumber(p.amount),
"minAmount" -> JsNumber(p.minAmount),
"price" -> JsNumber(p.price),
"volume" -> JsNumber(p.volume)
)
})
})
}
def reads(json: JsValue): JsResult[List[List[Position]]] = ???
}
"Application" should {
"Convert position data to json" in {
val l1 = ListBuffer(new Position(1.0D,2.0D,3.0D,4.0D),
new Position(5.0D,6.0D,7.0D,8.0D),
new Position(9.0D,10.0D,11.0D,12.0D)).toList
val l2 = ListBuffer(new Position(0.1D,0.2D,0.3D,0.4D),
new Position(5.0D,6.0D,7.0D,8.0D),
new Position(9.0D,10.0D,11.0D,12.0D)).toList
val obj = ListBuffer(l1,l2).toList
val json = Json.toJson(obj)
var string: String = json.toString()
println(string)
}
}
}
It seems that Json.arr takes it's arguments and returns a JsValue for a JSON array of them. It looks like you could simply do with Json.toJson:
Here's how arr is meant to be used:
// Json.arr
Json.arr("one", "two")
// Gives you
// play.api.libs.json.JsArray = ["one","two"]
If you instead do:
// vs
val l = List("one", "two")
Json.arr(l)
// Gives you
// play.api.libs.json.JsArray = [["one","two"]]
// ... a nested array, which is what you don't want.
What you need is:
// Json.toJson
Json.toJson(l)
// Gives you:
// play.api.libs.json.JsValue = ["one","two"]