Kotlinx Serialization - Custom serializer to ignore null value
Asked Answered
C

6

15

Let's say I'm having a class like:

@Serializable
data class MyClass(
    @SerialName("a") val a: String?,
    @SerialName("b") val b: String
)

Assume the a is null and b's value is "b value", then Json.stringify(MyClass.serializer(), this) produces:

{ "a": null, "b": "b value" }

Basically if a is null, I wanted to get this:

{ "b": "b value" }

From some research I found this is currently not doable out of the box with Kotlinx Serialization so I was trying to build a custom serializer to explicitly ignore null value. I followed the guide from here but couldn't make a correct one.

Can someone please shed my some light? Thanks.

Cicada answered 23/9, 2019 at 6:4 Comment(0)
P
2

Try this (not tested, just based on adapting the example):

@Serializable
data class MyClass(val a: String?, val b: String) {
    @Serializer(forClass = MyClass::class)
        companion object : KSerializer<MyClass> {
        override val descriptor: SerialDescriptor = object : SerialClassDescImpl("MyClass") {
            init {
                addElement("a")
                addElement("b")
            }
        }

        override fun serialize(encoder: Encoder, obj: MyClass) {
            encoder.beginStructure(descriptor).run {
                obj.a?.let { encodeStringElement(descriptor, 0, obj.a) }
                encodeStringElement(descriptor, 1, obj.b)
                endStructure(descriptor)
            }
        }

        override fun deserialize(decoder: Decoder): MyClass {
            var a: String? = null
            var b = ""

            decoder.beginStructure(descriptor).run {
                loop@ while (true) {
                    when (val i = decodeElementIndex(descriptor)) {
                        CompositeDecoder.READ_DONE -> break@loop
                        0 -> a = decodeStringElement(descriptor, i)
                        1 -> b = decodeStringElement(descriptor, i)
                        else -> throw SerializationException("Unknown index $i")
                    }
                }
                endStructure(descriptor)
            }

            return MyClass(a, b)
        }
    }
}
Pravit answered 23/9, 2019 at 9:10 Comment(0)
P
13

You can use explicitNulls = false

example:

@OptIn(ExperimentalSerializationApi::class)
val format = Json { explicitNulls = false }
    
@Serializable
data class Project(
    val name: String,
    val language: String,
    val version: String? = "1.3.0",
    val website: String?,
)
    
fun main() {
    val data = Project("kotlinx.serialization", "Kotlin", null, null)
    val json = format.encodeToString(data)
    println(json) // {"name":"kotlinx.serialization","language":"Kotlin"}
}

https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#explicit-nulls

Propylaeum answered 19/11, 2021 at 10:47 Comment(0)
C
3

Use encodeDefaults = false property in JsonConfiguration and it won't serialize nulls (or other optional values)

Cacodemon answered 28/5, 2020 at 19:30 Comment(1)
This only works if you want to ignore all default values which isn't always the case. One example is WooCommerce where there is a "variation_id" if it doesn't exist you shouldn't pass that into an order as a line item, so the ideal path is to set it to null but WooCommerce doesn't accept null as a value. In this case it'd be nice for more fine grained control to say "ignore this value if it hasn't been set" or "ignore this value if it is null".Hughmanick
P
2

Try this (not tested, just based on adapting the example):

@Serializable
data class MyClass(val a: String?, val b: String) {
    @Serializer(forClass = MyClass::class)
        companion object : KSerializer<MyClass> {
        override val descriptor: SerialDescriptor = object : SerialClassDescImpl("MyClass") {
            init {
                addElement("a")
                addElement("b")
            }
        }

        override fun serialize(encoder: Encoder, obj: MyClass) {
            encoder.beginStructure(descriptor).run {
                obj.a?.let { encodeStringElement(descriptor, 0, obj.a) }
                encodeStringElement(descriptor, 1, obj.b)
                endStructure(descriptor)
            }
        }

        override fun deserialize(decoder: Decoder): MyClass {
            var a: String? = null
            var b = ""

            decoder.beginStructure(descriptor).run {
                loop@ while (true) {
                    when (val i = decodeElementIndex(descriptor)) {
                        CompositeDecoder.READ_DONE -> break@loop
                        0 -> a = decodeStringElement(descriptor, i)
                        1 -> b = decodeStringElement(descriptor, i)
                        else -> throw SerializationException("Unknown index $i")
                    }
                }
                endStructure(descriptor)
            }

            return MyClass(a, b)
        }
    }
}
Pravit answered 23/9, 2019 at 9:10 Comment(0)
G
1

JsonConfiguration is deprecated in favor of Json {} builder since kotlinx.serialization 1.0.0-RC according to its changelog.

Now you have to code like this:

val json = Json { encodeDefaults = false }
val body = json.encodeToString(someSerializableObject)
Good answered 22/11, 2020 at 20:26 Comment(0)
H
1

Since I was also struggling with this one let me share with you the solution I found that is per property and does not require to create serializer for the whole class.

class ExcludeIfNullSerializer : KSerializer<String?> {

    override fun deserialize(decoder: Decoder): String {
        return decoder.decodeString()
    }

    override val descriptor: SerialDescriptor
        get() = PrimitiveSerialDescriptor("ExcludeNullString", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: String?) {
        if (value != null) {
            encoder.encodeString(value)
        }
    }
}

will work as expected with the following class

@Serializable
    class TestDto(
        @SerialName("someString")
        val someString: String,
        @SerialName("id")
        @EncodeDefault(EncodeDefault.Mode.NEVER)
        @Serializable(with = ExcludeIfNullSerializer::class)
        val id: String? = null
    )

Note the @EncodeDefault(EncodeDefault.Mode.NEVER) is crucial here in case you using JsonBuilder with encodeDefaults = true, as in this case the serialization library will still add the 'id' json key even if the value of id field is null unless using this annotation.

Hospitable answered 15/7, 2022 at 14:45 Comment(0)
U
1

As of now, for anyone seeing this pos today, default values are not serialized (see https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/basic-serialization.md#defaults-are-not-encoded-by-default)

So you simply add to set a default null value, and it will not be serialized.

Unexacting answered 8/11, 2022 at 10:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.