-
Couldn't load subscription status.
- Fork 662
RFC: Allow disabling coercion between JSON primitives #3063
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
RFC: Allow disabling coercion between JSON primitives #3063
Conversation
| consumeOther(allowQuoted = true) | ||
|
|
||
| fun consumeOther(allowQuoted: Boolean): String { | ||
| // TODO this string peeking stuff might break things... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the code around peekedString I'm pretty sure this is not actually a problem, because this branch would only ever be hit when attempting to read a string. Right?
8cc0590 to
2e5185f
Compare
|
Looks like a good idea to me |
2e5185f to
fb21578
Compare
|
I just rebased this onto latest dev. @sandwwraith @Tapchicoma Is there any chance to get feedback on the "does this have a chance of getting pulled?" question? I've been using this in production in a cloud service, so it's seen quite a bit of traffic without any issues so far. |
fb21578 to
2184b02
Compare
|
I've rebased this again. @sandwwraith @fzhinkin Have I completely missed some process here? Any feedback would be appreciated. |
|
Hi! Sorry, it took me a long time to check out your proposal. In general, I like the idea of adding such a setting. It would indeed help with stricter requirements and migrating between frameworks, such as Jackson. However, there are several key points that have to be addressed:
What do you think of this? If you want to continue working, I'd happily review your contribution. |
|
The choice of how to handle/serialize maps with non-string keys is ultimately a choice for the format to make (as there is no "native" mapping). The current behaviour to use quoted strings is probably the most elegant (although a strict parameter could prohibit it altogether). Fundamentally I think that the behaviour of map serialization with non-string keys is a separate one (where configuration may specify an alternative approach as for complex keys). The case for non-string primitive keys is that (to support them at all) the format maps them to strings and serializes/deserializes them as strings. For map values, they should follow the coercion rule ( |
|
@sandwwraith thanks for your feedback. Yes I'm still interested in working on this.
What issue are you referring to? Decoding from JsonElement already works with my changes, doesn't it?
May intention wasn't to touch anything regarding that behavior in this PR. as @pdvrieze suggests I think this is a separate topic for a separate PR. My intention here was merely to make primitive decoding, where it happens, more strict. I think it's best to go in smaller steps here.
True! I'll have a look at that. I have a few lose ends to tie up before I can work on this again, but I definitely will. We currently use this via a fork in production, so there is a strong interest in getting this upstream :) |
You're right, I overlooked your changes there.
Well, since code path for @Test
fun testStrictParsing() {
val j = Json {
allowPrimitiveCoercion = false
}
val input = """{"42": 42}"""
println(j.decodeFromString<Map<Int, Int>>( input))
}I do not mind this while the flag is experimental, but we would need to make a decision whether this behavior is desirable or if we should allow such deserialization. But I agree, it is possible to do it later.
Glad to hear that! |
We ran into one such case in our codebase (actually it was a value class that wraps an int) with this branch and solved it by implementing a dedicated "map key serializer": @Serializable
@JvmInline
value class IntWrapper(val value: Int)
private object IntWrapperKeySerializer : KSerializer<IntWrapper> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("IntWrapperMapKey", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: IntWrapper) = encoder.encodeString(value.value.toString())
override fun deserialize(decoder: Decoder) = IntWrapper(decoder.decodeString().toInt())
}
typealias IntWrapperStringMap = Map<
@Serializable(with = IntWrapperKeySerializer::class)
IntWrapper,
String,
>
@Serializable
data class Foo(
val map: IntWrapperStringMap
)Coming from Jackson, this didn't seem too surprising, since you also need dedicated map key (de)serializers there. |
|
@sandwwraith Ahh now I understand what you meant. Honestly I think it's the only logical consequence of enabling this flag, that keys must be decoded as strings, if necessary by using custom deserializer. With the great support for value classes this almost feels like a non-issue honestly. We should probably flesh out the documentation on the option to explicitly state this property, but I would argue that people looking for such a strict configuration are expecting this anyway. |
Currently, and independent of the
isLenientoption, the JSON decoders will coerce Strings to other primitiv types, allowing e.g."true"to be decoded asBoolean. We are currently transitioning from a very strict decoder setup based on Jackson over to kotlinx serialization for some of the nicer kotlin-specific functionality and the lack of reflection.This PR is currently meant as an RFC to gauge if this is something that would be accepted upstream. As such there are some caveats:
@ExperimentalSerializationApi, but it is not currently.I would obviously add those things, if there is a chance this would be pulled.
The implementation is rather simple: Introduce a boolean option and in and around the lexer prevent the use of quoted strings, where they wouldn't be necessary unless coercion is wanted. In some places the necessary functionality was already in place as separate methods (decodeBoolean vs decodeBooleanLenient), in others I was able to add minimal changes, usually as part of existing checks around string quotes. I tried to keep the interfaces stable.
Related issues: