kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

Delegate to default Serializer

Open timrijckaert opened this issue 5 years ago • 7 comments

Hi,

I'm dealing with an annoying JSON service which is out of my control.
If a value is unavailable it is expressed as an empty array [] instead of a canonical absence or null value.

I thought about making a custom JSON deserialiser and manually check if the value was present or not before delegating it to the default generated deserialiser. However when defining a custom deserialiser it seems the generated deserialiser is not generated no more.

I made a simple test case here:

public object SomeObjSerializer : KSerializer<SomeObj> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("SomeObj")

    override fun serialize(encoder: Encoder, value: SomeObj) {}

    override fun deserialize(decoder: Decoder): SomeObj {
        val input = decoder as JsonDecoder
        val jsonObj = input.decodeJsonElement().jsonObject
        return if (jsonObj["value"] is JsonPrimitive) {
            //How to delegate to the default generated serializer?
        } else {
            SomeObj(null)
        }
    }
}

@Serializable(with = SomeObjSerializer::class)
public data class SomeObj(val value: String?)

public fun main() {
    //language=JSON
    val someJson = """{"value" : [] }"""
    val clazz = Json.decodeFromString<SomeObj>(someJson)
    
    //language=JSON
    val someOtherJson = """{"value" : "42" }"""
    val clazz2 = Json.decodeFromString<SomeObj>(someOtherJson)
}

Note that this example is a simple use case.

As a temporary solution I made a copy of my object without specifying the custom deserialiser which I reference in my own deserialiser and then map back to my original object.
However that of course does not seem to be a good solution.

public object SomeObjSerializer : KSerializer<SomeObj> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("SomeObj")

    override fun serialize(encoder: Encoder, value: SomeObj) {}

    override fun deserialize(decoder: Decoder): SomeObj {
        val input = decoder as JsonDecoder
        val jsonObj = input.decodeJsonElement().jsonObject
        return if (jsonObj["value"] is JsonPrimitive) {
            val obj = decoder.decodeSerializableValue(CopyOfSomeObjForDelegationPurposedOnly.serializer())
            SomeObj(obj.value)
        } else {
            SomeObj(null)
        }
    }
}

@Serializable(with = SomeObjSerializer::class)
public data class SomeObj(val value: String?)
@Serializable
public data class CopyOfSomeObjForDelegationPurposedOnly(val value: String)

public fun main() {
    //language=JSON
    val someJson = """{"value" : [] }"""
    val clazz = Json.decodeFromString<SomeObj>(someJson)

    //language=JSON
    val someOtherJson = """{"value" : "42" }"""
    val clazz2 = Json.decodeFromString<SomeObj>(someOtherJson)
}

timrijckaert avatar Dec 13 '20 12:12 timrijckaert

Related: #1169

Btw, in your case It is worth looking into json transformers rather than full-blown custom serializers

sandwwraith avatar Dec 17 '20 13:12 sandwwraith

How can I return null?

public object ProgramSerializer : JsonTransformingSerializer<Program>(Program.serializer()) {
    override fun transformDeserialize(element: JsonElement): JsonElement =
        if (element is JsonObject) {
            element
        } else {
            null
        }
}
@Serializable
public data class SomeObject(
    @Serializable(with = ProgramSerializer::class)
    val program: Program? = null,
)

I tried returning JsonNull but it fails.

kotlinx.serialization.json.internal.JsonDecodingException: Expected class kotlinx.serialization.json.JsonObject as the serialized body of xxx.xxxx.Program, but had class kotlinx.serialization.json.JsonNull

timrijckaert avatar Dec 28 '20 20:12 timrijckaert

Hello?

timrijckaert avatar Jan 14 '21 11:01 timrijckaert

Unfortunately, JsonTransformingSerializer does not currently support nullable types (you can't write something like JsonTransformingSerializer<Program?>(Program.serializer().nullable).

shanshin avatar Jan 14 '21 15:01 shanshin

a temporary solution if you only need to deserialize, you can copy the logic from JsonTransformingSerializer

abstract class NullableJsonTransformingSerializer<T : Any>(
    private val tSerializer: KSerializer<T>
) : KSerializer<T?> {

    override val descriptor: SerialDescriptor get() = tSerializer.descriptor

    final override fun deserialize(decoder: Decoder): T? {
        require(decoder is JsonDecoder)
        val element = decoder.decodeJsonElement()
        return nullableTransformDeserialize(element)?.let { decoder.json.decodeFromJsonElement(tSerializer, it) }
    }

    protected open fun nullableTransformDeserialize(element: JsonElement): JsonElement? = element

    final override fun serialize(encoder: Encoder, value: T?) {
        throw IllegalAccessError("serialize not supported")
    }
}

unfortunately serialize requires internal calls to writeJson

ouchadam avatar Sep 02 '21 10:09 ouchadam

Hi there, I'm running into the same use case. Are there any plans to support nullable types with JsonTransformingSerializer with nullable types? It seems like a pretty common scenario, unless I'm missing an alternative way to do the same without the Json serializer.

The only way I can see around this for the time being (when serialization is also required, otherwise @ouchadam 's solution should work) is to replace the nullable type with a non-null one, and use a special Null "marker object" as model. But that's cumbersome and not always possible.

Thank you!

marcosalis avatar Apr 28 '22 11:04 marcosalis

I think nullable JsonTransformingSerializer is a separate issue, can you please create one?

sandwwraith avatar May 04 '22 13:05 sandwwraith