Skip to content

Commit

Permalink
Stabilize encoding and decoding of value classes (#1963)
Browse files Browse the repository at this point in the history
  • Loading branch information
qwwdfsad committed Jun 24, 2022
1 parent 93a06df commit 3e8331c
Show file tree
Hide file tree
Showing 14 changed files with 58 additions and 81 deletions.
2 changes: 1 addition & 1 deletion core/build.gradle
Expand Up @@ -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.
Expand Down
Expand Up @@ -194,28 +194,24 @@ public fun <K, V> MapSerializer(
/**
* Returns serializer for [UInt].
*/
@ExperimentalSerializationApi
@ExperimentalUnsignedTypes
public fun UInt.Companion.serializer(): KSerializer<UInt> = UIntSerializer

/**
* Returns serializer for [ULong].
*/
@ExperimentalSerializationApi
@ExperimentalUnsignedTypes
public fun ULong.Companion.serializer(): KSerializer<ULong> = ULongSerializer

/**
* Returns serializer for [UByte].
*/
@ExperimentalSerializationApi
@ExperimentalUnsignedTypes
public fun UByte.Companion.serializer(): KSerializer<UByte> = UByteSerializer

/**
* Returns serializer for [UShort].
*/
@ExperimentalSerializationApi
@ExperimentalUnsignedTypes
public fun UShort.Companion.serializer(): KSerializer<UShort> = UShortSerializer

Expand Down
Expand Up @@ -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

/**
Expand Down
Expand Up @@ -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 <T : Any?> decodeSerializableValue(
Expand Down
Expand Up @@ -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) }
Expand Down
47 changes: 20 additions & 27 deletions core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -571,6 +567,3 @@ public inline fun <T> 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."
43 changes: 19 additions & 24 deletions core/commonMain/src/kotlinx/serialization/encoding/Encoding.kt
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Expand Up @@ -10,7 +10,6 @@ import kotlinx.serialization.encoding.*

@Suppress("Unused")
@PublishedApi
@OptIn(ExperimentalSerializationApi::class)
internal class InlineClassDescriptor(
name: String,
generatedSerializer: GeneratedSerializer<*>
Expand Down
8 changes: 4 additions & 4 deletions core/commonMain/src/kotlinx/serialization/internal/Tagged.kt
Expand Up @@ -51,8 +51,8 @@ public abstract class TaggedEncoder<Tag : Any?> : 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 ----

Expand Down Expand Up @@ -209,8 +209,8 @@ public abstract class TaggedDecoder<Tag : Any?> : 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 {
Expand Down
Expand Up @@ -10,7 +10,6 @@ import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*

@PublishedApi
@ExperimentalSerializationApi
@ExperimentalUnsignedTypes
internal object UIntSerializer : KSerializer<UInt> {
override val descriptor: SerialDescriptor = InlinePrimitiveDescriptor("kotlin.UInt", Int.serializer())
Expand All @@ -25,7 +24,6 @@ internal object UIntSerializer : KSerializer<UInt> {
}

@PublishedApi
@ExperimentalSerializationApi
@ExperimentalUnsignedTypes
internal object ULongSerializer : KSerializer<ULong> {
override val descriptor: SerialDescriptor = InlinePrimitiveDescriptor("kotlin.ULong", Long.serializer())
Expand All @@ -40,7 +38,6 @@ internal object ULongSerializer : KSerializer<ULong> {
}

@PublishedApi
@ExperimentalSerializationApi
@ExperimentalUnsignedTypes
internal object UByteSerializer : KSerializer<UByte> {
override val descriptor: SerialDescriptor = InlinePrimitiveDescriptor("kotlin.UByte", Byte.serializer())
Expand All @@ -55,7 +52,6 @@ internal object UByteSerializer : KSerializer<UByte> {
}

@PublishedApi
@ExperimentalSerializationApi
@ExperimentalUnsignedTypes
internal object UShortSerializer : KSerializer<UShort> {
override val descriptor: SerialDescriptor = InlinePrimitiveDescriptor("kotlin.UShort", Short.serializer())
Expand Down
Expand Up @@ -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())
Expand Down
Expand Up @@ -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(
Expand All @@ -22,7 +21,6 @@ private val unsignedNumberDescriptors = setOf(
UShort.serializer().descriptor
)

@ExperimentalSerializationApi
internal val SerialDescriptor.isUnsignedNumber: Boolean
get() = this.isInline && this in unsignedNumberDescriptors

Expand Down Expand Up @@ -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
Expand Down
Expand Up @@ -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(
Expand Down
Expand Up @@ -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 {
Expand Down

0 comments on commit 3e8331c

Please sign in to comment.