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

Serializer coerce string to int

Open yackermann opened this issue 5 years ago • 15 comments

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

  • Kotlin version: [e.g. 1.4.0-RC]
  • Library version: [e.g. 1.0.0-RC]
  • Kotlin platforms: Multiplatform
  • Gradle version: [e.g. 6.5.1]

yackermann avatar Sep 06 '20 09:09 yackermann

Same thing with ints to strings

yackermann avatar Sep 06 '20 20:09 yackermann

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?

elizarov avatar Sep 07 '20 07:09 elizarov

@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. *(

yackermann avatar Sep 07 '20 10:09 yackermann

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?

elizarov avatar Sep 07 '20 14:09 elizarov

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

yackermann avatar Sep 07 '20 14:09 yackermann

What else? Where can we read more?

elizarov avatar Sep 07 '20 15:09 elizarov

What @elizarov what do you mean?

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

yackermann avatar Sep 08 '20 08:09 yackermann

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.

elizarov avatar Sep 08 '20 12:09 elizarov

@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

qwwdfsad avatar Sep 11 '20 14:09 qwwdfsad

@qwwdfsad

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

yackermann avatar Sep 12 '20 18:09 yackermann

@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).

gerob311 avatar Oct 30 '20 04:10 gerob311

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

cies avatar Apr 04 '22 21:04 cies

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")
    }
}

juggernaut0 avatar Mar 24 '24 22:03 juggernaut0