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

Support deserialization of top-level nullable types #1038

Merged
merged 1 commit into from
Sep 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ public sealed class Hocon(
return !conf.getIsNull(tag)
}

override fun decodeNotNullMark(): Boolean {
// Tag might be null for top-level deserialization
val currentTag = currentTagOrNull ?: return !conf.isEmpty
return decodeTaggedNotNullMark(currentTag)
}

override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder =
when {
descriptor.kind.listLike -> ListConfigReader(conf.getList(currentTag))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package kotlinx.serialization.hocon

import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import org.junit.*
import org.junit.Assert.*

Expand Down Expand Up @@ -79,6 +80,19 @@ class HoconValuesTest {
assertEquals(null, obj.s)
}

@Test
fun `deserialize nullable types with nullable serializer`() {
val obj = deserializeConfig("i = 10, s = null", WithNullable.serializer().nullable)!!
assertEquals(10, obj.i)
assertEquals(null, obj.s)
}

@Test
fun testDeserializerTopLevelNullableType() {
val value = deserializeConfig("", WithNullable.serializer().nullable)
assertNull(value)
}

@Test
fun `deserialize complex nullable values`() {
val configString = "i1 = [1,null,3], i2=null, i3 = [null, {i: 10, s: bar}]"
Expand Down
8 changes: 1 addition & 7 deletions runtime/api/kotlinx-serialization-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -1043,23 +1043,17 @@ public class kotlinx/serialization/internal/Migration : kotlinx/serialization/de

public abstract class kotlinx/serialization/internal/NamedValueDecoder : kotlinx/serialization/internal/TaggedDecoder {
public fun <init> ()V
public fun <init> (Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
protected fun composeName (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
protected fun elementName (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/String;
protected final fun getRootName ()Ljava/lang/String;
public synthetic fun getTag (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/Object;
protected final fun getTag (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/String;
protected final fun nested (Ljava/lang/String;)Ljava/lang/String;
}

public abstract class kotlinx/serialization/internal/NamedValueEncoder : kotlinx/serialization/internal/TaggedEncoder {
public fun <init> ()V
public fun <init> (Ljava/lang/String;)V
public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
protected fun composeName (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
protected fun elementName (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/String;
protected final fun getRootName ()Ljava/lang/String;
public synthetic fun getTag (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/Object;
protected final fun getTag (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Ljava/lang/String;
protected final fun nested (Ljava/lang/String;)Ljava/lang/String;
Expand Down Expand Up @@ -1229,7 +1223,7 @@ public abstract class kotlinx/serialization/internal/TaggedDecoder : kotlinx/ser
public final fun decodeIntElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)I
public final fun decodeLong ()J
public final fun decodeLongElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)J
public final fun decodeNotNullMark ()Z
public fun decodeNotNullMark ()Z
public final fun decodeNull ()Ljava/lang/Void;
public fun decodeNullableSerializableElement (Lkotlinx/serialization/descriptors/SerialDescriptor;ILkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object;
public final fun decodeNullableSerializableElement (Lkotlinx/serialization/descriptors/SerialDescriptor;ILkotlinx/serialization/DeserializationStrategy;Ljava/lang/Object;)Ljava/lang/Object;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import kotlinx.serialization.encoding.*
* @suppress internal API
*/
@PublishedApi
@OptIn(ExperimentalSerializationApi::class)
internal class NullableSerializer<T : Any>(private val serializer: KSerializer<T>) : KSerializer<T?> {
override val descriptor: SerialDescriptor = SerialDescriptorForNullable(serializer.descriptor)

Expand Down Expand Up @@ -42,6 +43,7 @@ internal class NullableSerializer<T : Any>(private val serializer: KSerializer<T
}
}

@OptIn(ExperimentalSerializationApi::class)
internal class SerialDescriptorForNullable(internal val original: SerialDescriptor) : SerialDescriptor by original {
override val serialName: String = original.serialName + "?"
override val isNullable: Boolean
Expand Down
22 changes: 15 additions & 7 deletions runtime/commonMain/src/kotlinx/serialization/internal/Tagged.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.encoding.updateModeDeprecated
import kotlinx.serialization.modules.*

/*
* These classes are intended to be used only within the kotlinx.serialization.
* They neither do have stable API, not internal invariants and are changed without any warnings.
* They neither do have stable API, nor internal invariants and are changed without any warnings.
*/
@InternalSerializationApi
public abstract class TaggedEncoder<Tag : Any?> : Encoder, CompositeEncoder {
Expand All @@ -32,7 +33,6 @@ public abstract class TaggedEncoder<Tag : Any?> : Encoder, CompositeEncoder {

protected open fun encodeTaggedNotNullMark(tag: Tag) {}
protected open fun encodeTaggedNull(tag: Tag): Unit = throw SerializationException("null is not supported")

protected open fun encodeTaggedInt(tag: Tag, value: Int): Unit = encodeTaggedValue(tag, value)
protected open fun encodeTaggedByte(tag: Tag, value: Byte): Unit = encodeTaggedValue(tag, value)
protected open fun encodeTaggedShort(tag: Tag, value: Short): Unit = encodeTaggedValue(tag, value)
Expand Down Expand Up @@ -167,9 +167,10 @@ public abstract class TaggedEncoder<Tag : Any?> : Encoder, CompositeEncoder {
}

@InternalSerializationApi
public abstract class NamedValueEncoder(protected val rootName: String = "") : TaggedEncoder<String>() {
@OptIn(ExperimentalSerializationApi::class)
public abstract class NamedValueEncoder : TaggedEncoder<String>() {
final override fun SerialDescriptor.getTag(index: Int): String = nested(elementName(this, index))
protected fun nested(nestedName: String): String = composeName(currentTagOrNull ?: rootName, nestedName)
protected fun nested(nestedName: String): String = composeName(currentTagOrNull ?: "", nestedName)
protected open fun elementName(descriptor: SerialDescriptor, index: Int): String = descriptor.getElementName(index)
protected open fun composeName(parentName: String, childName: String): String =
if (parentName.isEmpty()) childName else "$parentName.$childName"
Expand Down Expand Up @@ -214,7 +215,13 @@ public abstract class TaggedDecoder<Tag : Any?> : Decoder,

// ---- Implementation of low-level API ----

final override fun decodeNotNullMark(): Boolean = decodeTaggedNotNullMark(currentTag)
// TODO this method should be overridden by any sane format that supports top-level nulls
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved
override fun decodeNotNullMark(): Boolean {
// Tag might be null for top-level deserialization
val currentTag = currentTagOrNull ?: return false
return decodeTaggedNotNullMark(currentTag)
}

final override fun decodeNull(): Nothing? = null

final override fun decodeBoolean(): Boolean = decodeTaggedBoolean(popTag())
Expand Down Expand Up @@ -327,10 +334,11 @@ public abstract class TaggedDecoder<Tag : Any?> : Decoder,
}

@InternalSerializationApi
public abstract class NamedValueDecoder(protected val rootName: String = "") : TaggedDecoder<String>() {
@OptIn(ExperimentalSerializationApi::class)
public abstract class NamedValueDecoder : TaggedDecoder<String>() {
final override fun SerialDescriptor.getTag(index: Int): String = nested(elementName(this, index))

protected fun nested(nestedName: String): String = composeName(currentTagOrNull ?: rootName, nestedName)
protected fun nested(nestedName: String): String = composeName(currentTagOrNull ?: "", nestedName)
protected open fun elementName(desc: SerialDescriptor, index: Int): String = desc.getElementName(index)
protected open fun composeName(parentName: String, childName: String): String =
if (parentName.isEmpty()) childName else "$parentName.$childName"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ private sealed class AbstractJsonTreeDecoder(
// Nothing
}

override fun decodeNotNullMark(): Boolean = currentObject() !is JsonNull

protected open fun getValue(tag: String): JsonPrimitive {
val currentElement = currentElement(tag)
return currentElement as? JsonPrimitive ?: throw JsonDecodingException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal fun <T> Json.writeJson(value: T, serializer: SerializationStrategy<T>):
return result
}

@ExperimentalSerializationApi
private sealed class AbstractJsonTreeEncoder(
final override val json: Json,
val nodeConsumer: (JsonElement) -> Unit
Expand Down Expand Up @@ -186,9 +187,13 @@ private class JsonTreeListEncoder(json: Json, nodeConsumer: (JsonElement) -> Uni
override fun getCurrent(): JsonElement = JsonArray(array)
}

@OptIn(ExperimentalSerializationApi::class)
internal inline fun <reified T : JsonElement> cast(value: JsonElement, descriptor: SerialDescriptor): T {
if (value !is T) {
throw JsonDecodingException(-1, "Expected ${T::class} as the serialized body of ${descriptor.serialName}, but had ${value::class}")
throw JsonDecodingException(
-1,
"Expected ${T::class} as the serialized body of ${descriptor.serialName}, but had ${value::class}"
)
}
return value
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package kotlinx.serialization.json

import kotlinx.serialization.*
import kotlin.test.*

class DecodeFromJsonElementTest {
@Serializable
data class A(val a: Int)

@Serializable
data class B(val a: A?)

@Test
fun testDecodeTopLevelNullable() {
val a = A(42)
val jsonElement = Json.encodeToJsonElement(a)
assertEquals(a, Json.decodeFromJsonElement<A?>(jsonElement))
}

@Test
fun topLevelNull() {
assertNull(Json.decodeFromJsonElement<A?>(JsonNull))
}

@Test
fun testInnerNullable() {
val b = B(A(42))
val json = Json.encodeToJsonElement(b)
assertEquals(b, Json.decodeFromJsonElement(json))
}

@Test
fun testInnerNullableNull() {
val b = B(null)
val json = Json.encodeToJsonElement(b)
assertEquals(b, Json.decodeFromJsonElement(json))
}

@Test
fun testPrimitive() {
assertEquals(42, Json.decodeFromJsonElement(JsonPrimitive(42)))
assertEquals(42, Json.decodeFromJsonElement<Int?>(JsonPrimitive(42)))
assertEquals(null, Json.decodeFromJsonElement<Int?>(JsonNull))
}

@Test
fun testNullableList() {
assertEquals(listOf(42), Json.decodeFromJsonElement<List<Int>?>(JsonArray(listOf(JsonPrimitive(42)))))
assertEquals(listOf(42), Json.decodeFromJsonElement<List<Int?>?>(JsonArray(listOf(JsonPrimitive(42)))))
assertEquals(listOf(42), Json.decodeFromJsonElement<List<Int?>>(JsonArray(listOf(JsonPrimitive(42)))))
// Nulls
assertEquals(null, Json.decodeFromJsonElement<List<Int>?>(JsonNull))
assertEquals(null, Json.decodeFromJsonElement<List<Int?>?>(JsonNull))
}
}