Kotlin / kotlinx.serialization

Kotlin multiplatform / multi-format serialization
Apache License 2.0
5.34k stars 618 forks source link

Serializer coerce string to int #1042

Open yackermann opened 4 years ago

yackermann commented 4 years ago

Describe the bug

When requiring type of Int in a serializable class, type coerce string to int without throwing an error.

To Reproduce Attach a code snippet or test data if possible.

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable 
class Person(val age: Int)

fun main() {
    val obj = Json.decodeFromString<Person>("{\"age\": \"22\"}")
    println(obj) // Person(age=22)
}

Expected behavior

EXCEPTION

Environment

yackermann commented 4 years ago

Same thing with ints to strings

elizarov commented 4 years ago

Please, check out isLenient setting for JSON. https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/json.md#lenient-parsing

Does it do what you need?

yackermann commented 4 years ago

@elizarov isLenient makes parser more flexible. For my project I need strict parsing.

I have tested with isLinient = false, and it produces the same result. *(

elizarov commented 4 years ago

Thanks! Now I see what you want. Why do you want it, though? What's wrong with accepting "22" for an integer in your case?

yackermann commented 4 years ago

@elizarov FIDO protocols require strict JSON parsing to get certification.

elizarov commented 4 years ago

What else? Where can we read more?

yackermann commented 4 years ago

What @elizarov what do you mean?

There are a lot of standards that require strict parsing of JSON for compliancy and certification.

elizarov commented 4 years ago

Can you, please, provide a link to such a standard to make sure we can study it and see what else we might be missing for compliance.

qwwdfsad commented 4 years ago

@herrjemand could you please also elaborate on what you have been using prior to kotlinx-serialization? I've checked Moshi, GSON and Jackson, none of them supports this functionality by default.

Jackson will provide CoercionConfig in the next release that supports a lot of various coercion strategies, but for now it's unavailable

yackermann commented 4 years ago

@qwwdfsad

As a start https://fidoalliance.org/specs/fido-uaf-v1.1-id-20170202/fido-uaf-protocol-v1.1-id-20170202.html

gerob311 commented 3 years ago

@qwwdfsad Actually, while trying to switch from Jackson to kotlinx.serialisation, I found one of my test cases broke due to this. Jackson does in fact, not allow strings to be treated as integers (as expected).

cies commented 2 years ago

I'm interested too in this feature. The reason: I have non-JSON documents i first transform to a JSON string and feed those to kotlinx.serialization. Hence all primitives are strings. The best place to coerce them in Json.decodeFromString<MyDto>(json) as that function knows the target types in MyDto

juggernaut0 commented 5 months ago

I actually have a non-Json case where I'd like coercion. I'm using the Properties module to turn maps into objects but unfortunately my maps are Map<String, String> not Map<String, Any>.

@Serializable
data class Foo(val n: Int)

val map = mapOf("n" to "5") // this actually comes from external source, I do not have control of types

Properties.decodeFromMap(Foo.serializer(), map) // throws ClassCastException
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
    at kotlinx.serialization.internal.TaggedDecoder.decodeTaggedInt(Tagged.kt:195)
    at kotlinx.serialization.internal.TaggedDecoder.decodeInt(Tagged.kt:226)
    at kotlinx.serialization.internal.IntSerializer.deserialize(Primitives.kt:125)
    at kotlinx.serialization.internal.IntSerializer.deserialize(Primitives.kt:121)
    at kotlinx.serialization.properties.Properties$InMapper.decodeSerializableValue(Properties.kt:114)
    at kotlinx.serialization.internal.TaggedDecoder.decodeSerializableValue(Tagged.kt:207)
    at kotlinx.serialization.internal.TaggedDecoder$decodeNullableSerializableElement$1.invoke(Tagged.kt:288)
    at kotlinx.serialization.internal.TaggedDecoder.tagBlock(Tagged.kt:294)
    at kotlinx.serialization.internal.TaggedDecoder.decodeNullableSerializableElement(Tagged.kt:286)

Workaround is to create a new custom Serializer and annotate. Would be nice to have something built in for this though.

object IntAsStringSerializer : KSerializer<Int> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("IntAsString", PrimitiveKind.STRING)

    override fun serialize(encoder: Encoder, value: Int) {
        encoder.encodeString(value.toString())
    }

    override fun deserialize(decoder: Decoder): Int {
        val stringValue = decoder.decodeString()
        return stringValue.toIntOrNull() ?: throw SerializationException("Cannot parse int from string")
    }
}