Skip to content
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

Stabilize explicitNulls feature: #2661

Merged
merged 3 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
review fixes
  • Loading branch information
sandwwraith committed May 14, 2024
commit 51f795e2958cd99e0d9066921ef109552983d585
2 changes: 1 addition & 1 deletion docs/json.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ The current list of supported invalid values is:
* unknown values for enums

If value is missing, it is replaced either with a default property value if it exists,
or with a `null` if [explicitNulls](#explicit-nulls) flag is set to `false` and a property is nullable.
or with a `null` if [explicitNulls](#explicit-nulls) flag is set to `false` and a property is nullable (for enums).

> This list may be expanded in the future, so that [Json] instance configured with this property becomes even more
> permissive to invalid value in the input, replacing them with defaults or nulls.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ class JsonCoerceInputValuesTest : JsonTestBase() {
val e: SampleEnum
)

@Serializable
data class NullableEnumWithoutDefault(
val e: SampleEnum?
)

@Serializable
data class NullableEnumWithDefault(
val e: SampleEnum? = SampleEnum.OptionC
Expand Down Expand Up @@ -157,8 +152,17 @@ class JsonCoerceInputValuesTest : JsonTestBase() {
fun testNullableEnumWithoutDefault() {
val j = Json(json) { explicitNulls = false }
parametrizedTest { mode ->
assertEquals(NullableEnumWithoutDefault(null), j.decodeFromString("{}"))
assertEquals(NullableEnumWithoutDefault(null), j.decodeFromString("""{"e":"incorrect"}"""))
assertEquals(NullableEnumHolder(null), j.decodeFromString("{}"))
assertEquals(NullableEnumHolder(null), j.decodeFromString("""{"enum":"incorrect"}"""))
}
}

@Test
fun testNullableEnumWithoutDefaultDoesNotCoerceExplicitly() {
val j = Json(json) { explicitNulls = true }
parametrizedTest { mode ->
assertFailsWith<SerializationException> { j.decodeFromString<NullableEnumHolder>("{}") }
assertFailsWith<SerializationException> { j.decodeFromString<NullableEnumHolder>("""{"enum":"incorrect"}""") }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ public class JsonBuilder internal constructor(json: Json) {
* 2. Property type is an enum type, but JSON value contains an unknown enum member.
*
* Coerced values are treated as missing; they are replaced either with a default property value if it exists, or with a `null` if [explicitNulls] flag
* is set to `false` and a property is nullable.
* is set to `false` and a property is nullable (for enums).
*
* Example of usage:
* ```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,20 @@ internal fun SerialDescriptor.getJsonNameIndexOrThrow(json: Json, name: String,
return index
}

/**
* Tries to coerce value according to the rules of [JsonConfiguration.coerceInputValues] and [JsonConfiguration.explicitNulls] flags:
*
* - If a property is optional (has default), has a non-nullable type, but input was `null` literal, property is coerced. (1)
* - If a property is enum, but input contained string which is not a valid enum constant (3) or a `null` literal (2):
* - Property is coerced in case it is optional AND non-nullable (5), or nullable AND `explicitNulls` is on (4).
*
* @param descriptor Descriptor of class that owns the property
* @param index The index of the element (property).
* @param peekNull A function to peek if the next JSON token is `null`. In case `consume` is true, should consume `null` from the input.
* @param peekString A function to peek the next JSON token as a string.
* @param onEnumCoercing A callback function to be executed when coercing an enum. Use it to discard incorrect enum constant from the input.
* @return `true` if value was coerced, `false` otherwise.
*/
@OptIn(ExperimentalSerializationApi::class)
internal inline fun Json.tryCoerceValue(
descriptor: SerialDescriptor,
Expand All @@ -118,17 +132,17 @@ internal inline fun Json.tryCoerceValue(
): Boolean {
val isOptional = descriptor.isElementOptional(index)
val elementDescriptor = descriptor.getElementDescriptor(index)
if (isOptional && !elementDescriptor.isNullable && peekNull(true)) return true
if (isOptional && !elementDescriptor.isNullable && peekNull(true)) return true // (1)
if (elementDescriptor.kind == SerialKind.ENUM) {
if (elementDescriptor.isNullable && peekNull(false)) {
if (elementDescriptor.isNullable && peekNull(false)) { // (2)
return false
}

val enumValue = peekString()
?: return false // if value is not a string, decodeEnum() will throw correct exception
val enumIndex = elementDescriptor.getJsonNameIndex(this, enumValue)
val coerceToNull = !configuration.explicitNulls && elementDescriptor.isNullable
if (enumIndex == CompositeDecoder.UNKNOWN_NAME && (isOptional || coerceToNull)) {
val enumIndex = elementDescriptor.getJsonNameIndex(this, enumValue) // (3)
val coerceToNull = !configuration.explicitNulls && elementDescriptor.isNullable // (4)
if (enumIndex == CompositeDecoder.UNKNOWN_NAME && (isOptional || coerceToNull)) { // (3, 4, 5)
onEnumCoercing()
return true
}
Expand Down