crumpf

    crumpf

    1 year ago
    I’ve been battling modeling serializable classes with optionals and defaults. I understand that default property values don’t get encoded by default. I also know that one can enable
    encodeDefaults
    in the Json serialization class so defaults do get encoded. But, this has the side effect of encoding
    null
    into the json for properties that have been given default null values which is not desirable for communicating with some backend systems. So, I was wondering others have settled on for conventions or patterns to deal with this kind of thing. For example, lets considering the following example:
    interface MyInterface {
        val instruction: String
        val requiredField: String
    }
    
    @Serializable
    data class MyDataClass(
        override val requiredField: String,
        val optionalField: String? = null
    ) : MyInterface {
        override val instruction: String = "doABarrelRoll"
    }
    Here, we’ve got MyInterface which describes the basic contract that all messages exchanged with a service should adopt. To see what the effects of
    encodeDefault
    being true/false, the following code
    val data = MyDataClass("foo")
    println(Json.encodeToString(data))
    println(Json { encodeDefaults = true }.encodeToString(data))
    would output:
    {"requiredField":"foo"}
    {"requiredField":"foo","optionalField":null,"instruction":"doABarrelRoll"}
    But, we desire:
    {
      "requiredField": "foo",
      "instruction": "doABarrelRoll"
    }
    I know that a custom Serializer can be written for MyDataClass which could ignore encoding null values for properties. But, this seems that it would need to be done for all classes that incorporate optional properties. Apart from custom Serializers, has anyone used other strategies or structures for being able to encode defaults while ignoring nulls?
    Jumping off from my original example, I discovered that I could switch to a var in the interface (
    instruction
    in this case) and in the implementing class use lateinit and make an assignment in an init block to get the desired output.
    interface MyOtherInterface {
        var instruction: String
        val requiredField: String
    }
    
    @Serializable
    data class MyOtherDataClass(
        override val requiredField: String,
        val optionalField: String? = null
    ) : MyOtherInterface {
        override lateinit var instruction: String
        init {
            instruction = "doAnotherBarrelRoll"
        }
    }
    val data = MyOtherDataClass("bar")
    println(Json.encodeToString(data))
    outputs:
    {
      "requiredField": "bar",
      "instruction": "doAnotherBarrelRoll"
    }
    So, I get the json I was hoping for, but this would require losing immutability on the
    instruction
    property and also require the author of each implementing class to know to follow this pattern.
    p

    pdvrieze

    1 year ago
    There is an annotation (
    @Required
    ) that you can put on your properties to stop them being omitted if they equal the default. (see https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/basic-serialization.md#transient-properties)
    crumpf

    crumpf

    1 year ago
    For as much time as I’ve spent poring over the documentation, I don’t know how I missed
    @Required
    . Probably because the documentation looks like it only addresses decoding. But, it does appear to work for encoding too. You’re my hero, @pdvrieze