From 3e8331cf914203571504290bcefcf0080a4a81d9 Mon Sep 17 00:00:00 2001 From: Vsevolod Tolstopyatov Date: Fri, 24 Jun 2022 16:41:45 +0200 Subject: [PATCH] Stabilize encoding and decoding of value classes (#1963) --- core/build.gradle | 2 +- .../builtins/BuiltinSerializers.kt | 4 -- .../descriptors/SerialDescriptor.kt | 4 +- .../serialization/encoding/AbstractDecoder.kt | 2 +- .../serialization/encoding/AbstractEncoder.kt | 2 +- .../serialization/encoding/Decoding.kt | 47 ++++++++----------- .../serialization/encoding/Encoding.kt | 43 ++++++++--------- .../internal/InlineClassDescriptor.kt | 1 - .../kotlinx/serialization/internal/Tagged.kt | 8 ++-- .../{InlineClasses.kt => ValueClasses.kt} | 4 -- .../json/internal/StreamingJsonDecoder.kt | 6 +-- .../json/internal/StreamingJsonEncoder.kt | 8 ++-- .../internal/ProtobufTaggedDecoder.kt | 4 +- .../internal/ProtobufTaggedEncoder.kt | 4 +- 14 files changed, 58 insertions(+), 81 deletions(-) rename core/commonMain/src/kotlinx/serialization/internal/{InlineClasses.kt => ValueClasses.kt} (94%) diff --git a/core/build.gradle b/core/build.gradle index 900c494fb..1c39c7e88 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -25,7 +25,7 @@ kotlin { These manifest values help kotlinx.serialization compiler plugin determine if it is compatible with a given runtime library. Plugin reads them during compilation. - Implementation-Version is used to determine whether runtime library supports a given plugin feature (e.g. inline classes serialization + Implementation-Version is used to determine whether runtime library supports a given plugin feature (e.g. value classes serialization in Kotlin 1.x may require runtime library version 1.y to work). Compiler plugin may enable or disable features by looking on Implementation-Version. diff --git a/core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt b/core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt index ddc427832..91052fe2c 100644 --- a/core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt +++ b/core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt @@ -194,28 +194,24 @@ public fun MapSerializer( /** * Returns serializer for [UInt]. */ -@ExperimentalSerializationApi @ExperimentalUnsignedTypes public fun UInt.Companion.serializer(): KSerializer = UIntSerializer /** * Returns serializer for [ULong]. */ -@ExperimentalSerializationApi @ExperimentalUnsignedTypes public fun ULong.Companion.serializer(): KSerializer = ULongSerializer /** * Returns serializer for [UByte]. */ -@ExperimentalSerializationApi @ExperimentalUnsignedTypes public fun UByte.Companion.serializer(): KSerializer = UByteSerializer /** * Returns serializer for [UShort]. */ -@ExperimentalSerializationApi @ExperimentalUnsignedTypes public fun UShort.Companion.serializer(): KSerializer = UShortSerializer diff --git a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt index 136df8c31..af7193070 100644 --- a/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt +++ b/core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt @@ -162,9 +162,9 @@ public interface SerialDescriptor { public val isNullable: Boolean get() = false /** - * Returns `true` if this descriptor describes a serializable inline class. + * Returns `true` if this descriptor describes a serializable value class which underlying value + * is serialized directly. */ - @ExperimentalSerializationApi public val isInline: Boolean get() = false /** diff --git a/core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt b/core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt index 8e2799c95..fad7ef87c 100644 --- a/core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt +++ b/core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt @@ -34,7 +34,7 @@ public abstract class AbstractDecoder : Decoder, CompositeDecoder { override fun decodeString(): String = decodeValue() as String override fun decodeEnum(enumDescriptor: SerialDescriptor): Int = decodeValue() as Int - override fun decodeInline(inlineDescriptor: SerialDescriptor): Decoder = this + override fun decodeInline(descriptor: SerialDescriptor): Decoder = this // overwrite by default public open fun decodeSerializableValue( diff --git a/core/commonMain/src/kotlinx/serialization/encoding/AbstractEncoder.kt b/core/commonMain/src/kotlinx/serialization/encoding/AbstractEncoder.kt index f197c40f4..384cb8a05 100644 --- a/core/commonMain/src/kotlinx/serialization/encoding/AbstractEncoder.kt +++ b/core/commonMain/src/kotlinx/serialization/encoding/AbstractEncoder.kt @@ -51,7 +51,7 @@ public abstract class AbstractEncoder : Encoder, CompositeEncoder { override fun encodeString(value: String): Unit = encodeValue(value) override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int): Unit = encodeValue(index) - override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder = this + override fun encodeInline(descriptor: SerialDescriptor): Encoder = this // Delegating implementation of CompositeEncoder final override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) { if (encodeElement(descriptor, index)) encodeBoolean(value) } diff --git a/core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt b/core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt index 3e93e3db8..f29c8057f 100644 --- a/core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt +++ b/core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt @@ -108,7 +108,7 @@ import kotlinx.serialization.modules.* * * ### Not stable for inheritance * - * `Decoder` interface is not stable for inheritance in 3rd party libraries, as new methods + * `Decoder` interface is not stable for inheritance in 3rd-party libraries, as new methods * might be added to this interface or contracts of the existing methods can be changed. */ public interface Decoder { @@ -211,27 +211,24 @@ public interface Decoder { public fun decodeEnum(enumDescriptor: SerialDescriptor): Int /** - * Returns [Decoder] for decoding an underlying type of an inline class. - * [inlineDescriptor] describes a target inline class. + * Returns [Decoder] for decoding an underlying type of a value class in an inline manner. + * [descriptor] describes a target value class. * - * Namely, for the `@Serializable inline class MyInt(val my: Int)`, - * the following sequence is used: + * Namely, for the `@Serializable @JvmInline value class MyInt(val my: Int)`, the following sequence is used: * ``` * thisDecoder.decodeInline(MyInt.serializer().descriptor).decodeInt() * ``` * - * Current decoder may return any other instance of [Decoder] class, - * depending on the provided [inlineDescriptor]. - * For example, when this function is called on Json decoder with - * `UInt.serializer().descriptor`, the returned decoder is able - * to decode unsigned integers. + * Current decoder may return any other instance of [Decoder] class, depending on the provided [descriptor]. + * For example, when this function is called on `Json` decoder with + * `UInt.serializer().descriptor`, the returned decoder is able to decode unsigned integers. * * Note that this function returns [Decoder] instead of the [CompositeDecoder] - * because inline classes always have the single property. - * Calling [Decoder.beginStructure] on returned instance leads to an undefined behavior. + * because value classes always have the single property. + * + * Calling [Decoder.beginStructure] on returned instance leads to an unspecified behavior and, in general, is prohibited. */ - @ExperimentalSerializationApi - public fun decodeInline(inlineDescriptor: SerialDescriptor): Decoder + public fun decodeInline(descriptor: SerialDescriptor): Decoder /** * Decodes the beginning of the nested structure in a serialized form @@ -488,35 +485,34 @@ public interface CompositeDecoder { public fun decodeStringElement(descriptor: SerialDescriptor, index: Int): String /** - * Returns [Decoder] for decoding an underlying type of an inline class. - * Serializable inline class is described by the [child descriptor][SerialDescriptor.getElementDescriptor] + * Returns [Decoder] for decoding an underlying type of a value class in an inline manner. + * Serializable value class is described by the [child descriptor][SerialDescriptor.getElementDescriptor] * of given [descriptor] at [index]. * - * Namely, for the `@Serializable inline class MyInt(val my: Int)`, - * and `@Serializable class MyData(val myInt: MyInt)` - * the following sequence is used: + * Namely, for the `@Serializable @JvmInline value class MyInt(val my: Int)`, + * and `@Serializable class MyData(val myInt: MyInt)` the following sequence is used: * ``` * thisDecoder.decodeInlineElement(MyData.serializer().descriptor, 0).decodeInt() * ``` * - * This method provides an opportunity for the optimization and its invocation should be identical to + * This method provides an opportunity for the optimization to avoid boxing of a carried value + * and its invocation should be equivalent to the following: * ``` * thisDecoder.decodeSerializableElement(MyData.serializer.descriptor, 0, MyInt.serializer()) * ``` * * Current decoder may return any other instance of [Decoder] class, depending on the provided descriptor. - * For example, when this function is called on Json decoder with descriptor that has + * For example, when this function is called on `Json` decoder with descriptor that has * `UInt.serializer().descriptor` at the given [index], the returned decoder is able * to decode unsigned integers. * * Note that this function returns [Decoder] instead of the [CompositeDecoder] - * because inline classes always have the single property. - * Calling [Decoder.beginStructure] on returned instance leads to an undefined behavior. + * because value classes always have the single property. + * Calling [Decoder.beginStructure] on returned instance leads to an unspecified behavior and, in general, is prohibited. * * @see Decoder.decodeInline * @see SerialDescriptor.getElementDescriptor */ - @ExperimentalSerializationApi public fun decodeInlineElement( descriptor: SerialDescriptor, index: Int @@ -571,6 +567,3 @@ public inline fun Decoder.decodeStructure( composite.endStructure(descriptor) return result } - -private const val decodeMethodDeprecated = "Please migrate to decodeElement method which accepts old value." + - "Feel free to ignore it if your format does not support updates." diff --git a/core/commonMain/src/kotlinx/serialization/encoding/Encoding.kt b/core/commonMain/src/kotlinx/serialization/encoding/Encoding.kt index 1113b1c70..2b1dd09cc 100644 --- a/core/commonMain/src/kotlinx/serialization/encoding/Encoding.kt +++ b/core/commonMain/src/kotlinx/serialization/encoding/Encoding.kt @@ -207,27 +207,24 @@ public interface Encoder { public fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) /** - * Returns [Encoder] for encoding an underlying type of an inline class. - * [inlineDescriptor] describes a serializable inline class. + * Returns [Encoder] for encoding an underlying type of a value class in an inline manner. + * [descriptor] describes a serializable value class. * - * Namely, for the `@Serializable inline class MyInt(val my: Int)`, + * Namely, for the `@Serializable @JvmInline value class MyInt(val my: Int)`, * the following sequence is used: * ``` * thisEncoder.encodeInline(MyInt.serializer().descriptor).encodeInt(my) * ``` * - * Current encoder may return any other instance of [Encoder] class, - * depending on the provided [inlineDescriptor]. - * For example, when this function is called on Json encoder with - * `UInt.serializer().descriptor`, the returned encoder is able + * Current encoder may return any other instance of [Encoder] class, depending on the provided [descriptor]. + * For example, when this function is called on Json encoder with `UInt.serializer().descriptor`, the returned encoder is able * to encode unsigned integers. * - * Note that this function returns [Encoder] instead of [CompositeEncoder] - * because inline classes always have one property. - * Calling [Encoder.beginStructure] on returned instance leads to an undefined behavior. + * Note that this function returns [Encoder] instead of the [CompositeEncoder] + * because value classes always have the single property. + * Calling [Encoder.beginStructure] on returned instance leads to an unspecified behavior and, in general, is prohibited. */ - @ExperimentalSerializationApi - public fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder + public fun encodeInline(descriptor: SerialDescriptor): Encoder /** * Encodes the beginning of the nested structure in a serialized form @@ -411,36 +408,34 @@ public interface CompositeEncoder { public fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) /** - * Returns [Encoder] for decoding an underlying type of an inline class. - * Serializable inline class is described by the [child descriptor][SerialDescriptor.getElementDescriptor] + * Returns [Encoder] for decoding an underlying type of a value class in an inline manner. + * Serializable value class is described by the [child descriptor][SerialDescriptor.getElementDescriptor] * of given [descriptor] at [index]. * - * Namely, for the `@Serializable inline class MyInt(val my: Int)`, - * and `@Serializable class MyData(val myInt: MyInt)` - * the following sequence is used: + * Namely, for the `@Serializable @JvmInline value class MyInt(val my: Int)`, + * and `@Serializable class MyData(val myInt: MyInt)` the following sequence is used: * ``` * thisEncoder.encodeInlineElement(MyData.serializer.descriptor, 0).encodeInt(my) * ``` * - * This method is an optimization and its invocation should have the exact same result as + * This method provides an opportunity for the optimization to avoid boxing of a carried value + * and its invocation should be equivalent to the following: * ``` * thisEncoder.encodeSerializableElement(MyData.serializer.descriptor, 0, MyInt.serializer(), myInt) * ``` * - * Current encoder may return any other instance of [Encoder] class, - * depending on provided descriptor. + * Current encoder may return any other instance of [Encoder] class, depending on provided descriptor. * For example, when this function is called on Json encoder with descriptor that has * `UInt.serializer().descriptor` at the given [index], the returned encoder is able * to encode unsigned integers. * - * Note that this function returns [Encoder] instead of [CompositeEncoder] - * because inline classes always have one property. - * Calling [Encoder.beginStructure] on returned instance leads to an undefined behavior. + * Note that this function returns [Encoder] instead of the [CompositeEncoder] + * because value classes always have the single property. + * Calling [Encoder.beginStructure] on returned instance leads to an unspecified behavior and, in general, is prohibited. * * @see Encoder.encodeInline * @see SerialDescriptor.getElementDescriptor */ - @ExperimentalSerializationApi public fun encodeInlineElement( descriptor: SerialDescriptor, index: Int diff --git a/core/commonMain/src/kotlinx/serialization/internal/InlineClassDescriptor.kt b/core/commonMain/src/kotlinx/serialization/internal/InlineClassDescriptor.kt index 05fd92be0..b5068477a 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/InlineClassDescriptor.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/InlineClassDescriptor.kt @@ -10,7 +10,6 @@ import kotlinx.serialization.encoding.* @Suppress("Unused") @PublishedApi -@OptIn(ExperimentalSerializationApi::class) internal class InlineClassDescriptor( name: String, generatedSerializer: GeneratedSerializer<*> diff --git a/core/commonMain/src/kotlinx/serialization/internal/Tagged.kt b/core/commonMain/src/kotlinx/serialization/internal/Tagged.kt index a02a04b26..9d7dd09f8 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/Tagged.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/Tagged.kt @@ -51,8 +51,8 @@ public abstract class TaggedEncoder : Encoder, CompositeEncoder { protected open fun encodeTaggedInline(tag: Tag, inlineDescriptor: SerialDescriptor): Encoder = this.apply { pushTag(tag) } - final override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder = - encodeTaggedInline(popTag(), inlineDescriptor) + final override fun encodeInline(descriptor: SerialDescriptor): Encoder = + encodeTaggedInline(popTag(), descriptor) // ---- Implementation of low-level API ---- @@ -209,8 +209,8 @@ public abstract class TaggedDecoder : Decoder, CompositeDecoder { // ---- Implementation of low-level API ---- - final override fun decodeInline(inlineDescriptor: SerialDescriptor): Decoder = - decodeTaggedInline(popTag(), inlineDescriptor) + final override fun decodeInline(descriptor: SerialDescriptor): Decoder = + decodeTaggedInline(popTag(), descriptor) // TODO this method should be overridden by any sane format that supports top-level nulls override fun decodeNotNullMark(): Boolean { diff --git a/core/commonMain/src/kotlinx/serialization/internal/InlineClasses.kt b/core/commonMain/src/kotlinx/serialization/internal/ValueClasses.kt similarity index 94% rename from core/commonMain/src/kotlinx/serialization/internal/InlineClasses.kt rename to core/commonMain/src/kotlinx/serialization/internal/ValueClasses.kt index b9738908c..b2657d656 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/InlineClasses.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/ValueClasses.kt @@ -10,7 +10,6 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @PublishedApi -@ExperimentalSerializationApi @ExperimentalUnsignedTypes internal object UIntSerializer : KSerializer { override val descriptor: SerialDescriptor = InlinePrimitiveDescriptor("kotlin.UInt", Int.serializer()) @@ -25,7 +24,6 @@ internal object UIntSerializer : KSerializer { } @PublishedApi -@ExperimentalSerializationApi @ExperimentalUnsignedTypes internal object ULongSerializer : KSerializer { override val descriptor: SerialDescriptor = InlinePrimitiveDescriptor("kotlin.ULong", Long.serializer()) @@ -40,7 +38,6 @@ internal object ULongSerializer : KSerializer { } @PublishedApi -@ExperimentalSerializationApi @ExperimentalUnsignedTypes internal object UByteSerializer : KSerializer { override val descriptor: SerialDescriptor = InlinePrimitiveDescriptor("kotlin.UByte", Byte.serializer()) @@ -55,7 +52,6 @@ internal object UByteSerializer : KSerializer { } @PublishedApi -@ExperimentalSerializationApi @ExperimentalUnsignedTypes internal object UShortSerializer : KSerializer { override val descriptor: SerialDescriptor = InlinePrimitiveDescriptor("kotlin.UShort", Short.serializer()) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt index 403e90deb..9c6ba7429 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt @@ -344,9 +344,9 @@ internal open class StreamingJsonDecoder( } } - override fun decodeInline(inlineDescriptor: SerialDescriptor): Decoder = - if (inlineDescriptor.isUnsignedNumber) JsonDecoderForUnsignedTypes(lexer, json) - else super.decodeInline(inlineDescriptor) + override fun decodeInline(descriptor: SerialDescriptor): Decoder = + if (descriptor.isUnsignedNumber) JsonDecoderForUnsignedTypes(lexer, json) + else super.decodeInline(descriptor) override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { return enumDescriptor.getJsonNameIndexOrThrow(json, decodeString(), " at path " + lexer.path.getPath()) diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt index 0381a1375..d2d9985b7 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt @@ -12,7 +12,6 @@ import kotlinx.serialization.json.* import kotlinx.serialization.modules.* import kotlin.native.concurrent.* -@ExperimentalSerializationApi @OptIn(ExperimentalUnsignedTypes::class) @SharedImmutable private val unsignedNumberDescriptors = setOf( @@ -22,7 +21,6 @@ private val unsignedNumberDescriptors = setOf( UShort.serializer().descriptor ) -@ExperimentalSerializationApi internal val SerialDescriptor.isUnsignedNumber: Boolean get() = this.isInline && this in unsignedNumberDescriptors @@ -158,11 +156,11 @@ internal class StreamingJsonEncoder( } } - override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder = - if (inlineDescriptor.isUnsignedNumber) StreamingJsonEncoder( + override fun encodeInline(descriptor: SerialDescriptor): Encoder = + if (descriptor.isUnsignedNumber) StreamingJsonEncoder( composerForUnsignedNumbers(), json, mode, null ) - else super.encodeInline(inlineDescriptor) + else super.encodeInline(descriptor) private fun composerForUnsignedNumbers(): Composer { // If we're inside encodeInline().encodeSerializableValue, we should preserve the forceQuoting state diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedDecoder.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedDecoder.kt index 8a5a38278..953c1b3ce 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedDecoder.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedDecoder.kt @@ -94,8 +94,8 @@ internal abstract class ProtobufTaggedDecoder : ProtobufTaggedBase(), Decoder, C } } - override fun decodeInline(inlineDescriptor: SerialDescriptor): Decoder { - return decodeTaggedInline(popTag(), inlineDescriptor) + override fun decodeInline(descriptor: SerialDescriptor): Decoder { + return decodeTaggedInline(popTag(), descriptor) } override fun decodeInlineElement( diff --git a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt index 01532df3f..84e58399f 100644 --- a/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt +++ b/formats/protobuf/commonMain/src/kotlinx/serialization/protobuf/internal/ProtobufTaggedEncoder.kt @@ -154,8 +154,8 @@ internal abstract class ProtobufTaggedEncoder : ProtobufTaggedBase(), Encoder, C encodeNullableSerializableValue(serializer, value) } - override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder { - return encodeTaggedInline(popTag(), inlineDescriptor) + override fun encodeInline(descriptor: SerialDescriptor): Encoder { + return encodeTaggedInline(popTag(), descriptor) } override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder {