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 encoding and decoding of value classes #1963

Merged
merged 4 commits into from Jun 24, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
qwwdfsad marked this conversation as resolved.
Show resolved Hide resolved

/**
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.
Copy link
Contributor

@whyoleg whyoleg Jun 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just interesting: in future value classes will be able to have multiple properties (multi-field value classes KEEP) - how then kx.serialization will work with them?
Even if for now inline class = value class, may be it will be better consider in mind, that it will change.
And, f.e. introduce separate annotation like InlineSerializable which will work not only with inline/value classes, but even with any other ordinary class or even interface?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how then kx.serialization will work with them

It's hard to tell without an actual design and implementation of multi-field value classes.

If the language will change the notion of value classes, we will of course have to change these methods accordingly

* 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