4

How to distinguish between {data: null} and {} in kotlinx.serialization when deserializing JSON?

@Serializable
class MyClass(val data: String?)
wilddev
  • 1,904
  • 2
  • 27
  • 45
  • 2
    You mean you are receiving this kind of JSON, and you want to represent it in Kotlin in a way that lets you tell the difference? – Joffrey May 25 '21 at 20:09
  • Your code sample won't even run for second case because `data` is not optional. You need to set a default value that will be used if `data` is absent and that is your answer. – Pawel May 25 '21 at 20:17
  • @Joffrey, that's exactly what I want – wilddev May 25 '21 at 20:22
  • 1
    I was researching exactly the same problem some time ago and unfortunately the conclusion was that kotlinx.serialization does not really support it without some hacks :-( You can always write your own serializer or do something like: `const val UNDEFINED = ""` and then declare your prop as: `val data: String? = UNDEFINED`. – broot May 25 '21 at 20:55
  • What means "distinguish"? You want them to be deserialized into different objects? How these objects you want to look like? – Михаил Нафталь May 25 '21 at 20:58
  • 1
    Anyway you may use [parseToJsonElement](https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx-serialization-json/kotlinx.serialization.json/-json/parse-to-json-element.html) method - it will return different objects for these JSONs – Михаил Нафталь May 25 '21 at 20:59
  • @МихаилНафталь, look at comment of user Joffrey. That is what I mean by "distinguish" – wilddev May 25 '21 at 21:00

2 Answers2

0

You need a custom setter and a private boolean field to indicate whether the field value was touched. So something like:

@Serializable
class MyClass {
    private var isDataTouched = false

    val data: String
        set(value) {        
            field = value
            isDataTouched = true
        }
}

Note, you can't define the field in the default constructor.

Sebi
  • 8,323
  • 6
  • 48
  • 76
0

You can take advantage of polymorphism coupled with JsonContentPolymorphicSerializer to distinguish what type of response you're deserializing:

@Serializable(with = MyResponseSerializer::class)
sealed class MyResponse

@Serializable
class MyEmptyClass : MyResponse()

@Serializable
class MyClass(val data: String?) : MyResponse()

// this serializer is reponsible for determining what class we will receive
// by inspecting json structure
object MyResponseSerializer : JsonContentPolymorphicSerializer<MyResponse>(MyResponse::class) {
    override fun selectDeserializer(element: JsonElement) = when {
        "data" in element.jsonObject -> MyClass.serializer()
        else -> MyEmptyClass.serializer()
    }
}

After that deserialize using MyResponse class and you will receive either MyEmptyClass or MyClass:

val e = Json.decodeFromString<MyResponse>("""{}""")             // returns MyEmptyClass object
val o = Json.decodeFromString<MyResponse>("""{"data": null}""") // returns MyClass object

println(Json.encodeToString(e)) // {}
println(Json.encodeToString(o)) // {"data": null}

// since sealed class is the parent we can have exhaustive when:
when(e) {
    is MyClass -> TODO("handle response with data")
    is MyEmptyClass -> TODO("handle empty response")
}
Pawel
  • 15,548
  • 3
  • 36
  • 36