Skip to content

Commit

Permalink
Introduce @AdvancedEncodingApi and @SealedSerializationApi annotations
Browse files Browse the repository at this point in the history
to be used with @SubclassOptInRequired.

These annotations allow for even more fine-grained API marking. We now can designate APIs
as public for use, but closed for implementation (@SealedSerializationApi) — the case for SerialDescriptor, which is a non-sealed interface for technical reasons.

The other annotation, @AdvancedEncodingApi is aimed to provide guidance on implementing custom encoders/decoders by pointing users to a documentation and guides.

Fixes #2366
  • Loading branch information
sandwwraith committed Aug 7, 2024
1 parent 0b3288f commit 3c7da48
Show file tree
Hide file tree
Showing 35 changed files with 115 additions and 11 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ val excludedFromBomProjects get() = unpublishedProjects + "kotlinx-serialization
val experimentalsEnabled get() = listOf(
"-progressive",
"-opt-in=kotlin.ExperimentalMultiplatform",
"-opt-in=kotlin.ExperimentalSubclassOptIn",
"-opt-in=kotlinx.serialization.InternalSerializationApi",
"-P", "plugin:org.jetbrains.kotlinx.serialization:disableIntrinsic=false"
)
Expand Down
4 changes: 4 additions & 0 deletions buildSrc/src/main/kotlin/source-sets-conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,10 @@ kotlin {
progressiveMode = true

optIn("kotlin.ExperimentalMultiplatform")
optIn("kotlin.ExperimentalSubclassOptIn")
optIn("kotlin.ExperimentalStdlibApi")
optIn("kotlinx.serialization.InternalSerializationApi")
optIn("kotlinx.serialization.SealedSerializationApi")
}
}

Expand Down Expand Up @@ -151,7 +153,9 @@ kotlin {
sourceSets.matching({ it.name.contains("Test") }).configureEach {
languageSettings {
optIn("kotlinx.serialization.InternalSerializationApi")
optIn("kotlinx.serialization.SealedSerializationApi")
optIn("kotlinx.serialization.ExperimentalSerializationApi")
optIn("kotlinx.serialization.encoding.AdvancedEncodingApi")
}
}

Expand Down
6 changes: 6 additions & 0 deletions core/api/kotlinx-serialization-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ public final class kotlinx/serialization/SealedClassSerializer : kotlinx/seriali
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
}

public abstract interface annotation class kotlinx/serialization/SealedSerializationApi : java/lang/annotation/Annotation {
}

public abstract interface class kotlinx/serialization/SerialFormat {
public abstract fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
}
Expand Down Expand Up @@ -423,6 +426,9 @@ public abstract class kotlinx/serialization/encoding/AbstractEncoder : kotlinx/s
public fun shouldEncodeElementDefault (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Z
}

public abstract interface annotation class kotlinx/serialization/encoding/AdvancedEncodingApi : java/lang/annotation/Annotation {
}

public abstract interface class kotlinx/serialization/encoding/ChunkedDecoder {
public abstract fun decodeStringChunked (Lkotlin/jvm/functions/Function1;)V
}
Expand Down
8 changes: 8 additions & 0 deletions core/api/kotlinx-serialization-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
// - Show declarations: true

// Library unique name: <org.jetbrains.kotlinx:kotlinx-serialization-core>
open annotation class kotlinx.serialization.encoding/AdvancedEncodingApi : kotlin/Annotation { // kotlinx.serialization.encoding/AdvancedEncodingApi|null[0]
constructor <init>() // kotlinx.serialization.encoding/AdvancedEncodingApi.<init>|<init>(){}[0]
}

open annotation class kotlinx.serialization.internal/NamedCompanion : kotlin/Annotation { // kotlinx.serialization.internal/NamedCompanion|null[0]
constructor <init>() // kotlinx.serialization.internal/NamedCompanion.<init>|<init>(){}[0]
}
Expand Down Expand Up @@ -61,6 +65,10 @@ open annotation class kotlinx.serialization/Required : kotlin/Annotation { // ko
constructor <init>() // kotlinx.serialization/Required.<init>|<init>(){}[0]
}

open annotation class kotlinx.serialization/SealedSerializationApi : kotlin/Annotation { // kotlinx.serialization/SealedSerializationApi|null[0]
constructor <init>() // kotlinx.serialization/SealedSerializationApi.<init>|<init>(){}[0]
}

open annotation class kotlinx.serialization/SerialInfo : kotlin/Annotation { // kotlinx.serialization/SerialInfo|null[0]
constructor <init>() // kotlinx.serialization/SerialInfo.<init>|<init>(){}[0]
}
Expand Down
14 changes: 14 additions & 0 deletions core/commonMain/src/kotlinx/serialization/Annotations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,17 @@ public annotation class ExperimentalSerializationApi
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS)
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
public annotation class InternalSerializationApi

/**
* Marks interfaces and non-final classes that can be freely referenced in users' code but should not be
* implemented or inherited. Such declarations are effectively `sealed` and do not have this modifier purely for technical reasons.
*
* kotlinx.serialization library provides compatibility guarantees for existing signatures of such classes;
* however, new functions or properties can be added to them in any release.
*/
@MustBeDocumented
@Target(AnnotationTarget.CLASS)
@RequiresOptIn(message = "This class or interface should not be inherited/implemented outside of kotlinx.serialization library. " +
"Note it is still permitted to use it directly. Read its documentation about inheritance for details.", level = RequiresOptIn.Level.ERROR)
public annotation class SealedSerializationApi

Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ import kotlinx.serialization.encoding.*
* might be added to this interface when kotlinx.serialization adds support for new Kotlin features.
* This interface is safe to use and construct via [buildClassSerialDescriptor], [PrimitiveSerialDescriptor], and `SerialDescriptor` factory function.
*/
// TODO: there was a phrase 'and is safe to delegate implementation to existing instances.' but it is not true unless we enable -Xjvm-default
@SubclassOptInRequired(SealedSerializationApi::class)
public interface SerialDescriptor {
/**
* Serial name of the descriptor that identifies a pair of the associated serializer and target class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import kotlinx.serialization.descriptors.*
* See [Decoder] documentation for information about each particular `decode*` method.
*/
@ExperimentalSerializationApi
@SubclassOptInRequired(AdvancedEncodingApi::class)
public abstract class AbstractDecoder : Decoder, CompositeDecoder {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import kotlinx.serialization.internal.*
* See [Encoder] documentation for information about each particular `encode*` method.
*/
@ExperimentalSerializationApi
@SubclassOptInRequired(AdvancedEncodingApi::class)
public abstract class AbstractEncoder : Encoder, CompositeEncoder {

override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder = this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ import kotlinx.serialization.modules.*
* `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.
*/
@SubclassOptInRequired(AdvancedEncodingApi::class)
public interface Decoder {
/**
* Context of the current serialization process, including contextual and polymorphic serialization and,
Expand Down Expand Up @@ -292,6 +293,7 @@ internal inline fun <T : Any> Decoder.decodeIfNullable(deserializer: Deserializa
* `CompositeDecoder` 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.
*/
@SubclassOptInRequired(AdvancedEncodingApi::class)
public interface CompositeDecoder {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ import kotlinx.serialization.modules.*
* `Encoder` 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.
*/
@SubclassOptInRequired(AdvancedEncodingApi::class)
public interface Encoder {
/**
* Context of the current serialization process, including contextual and polymorphic serialization and,
Expand Down Expand Up @@ -320,6 +321,7 @@ public interface Encoder {
* `CompositeEncoder` 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.
*/
@SubclassOptInRequired(AdvancedEncodingApi::class)
public interface CompositeEncoder {
/**
* Context of the current serialization process, including contextual and polymorphic serialization and,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.encoding

/**
* Marks all encoding- and decoding-related interfaces in kotlinx.serialization.
* These interfaces are used in serializers and have to be implemented only if you want to write
* a custom serialization format. Since encoder/decoder invariants are quite complex,
* it is recommended to start with reading their documentation: see [Encoder] and [Decoder],
* and [kotlinx.serialization guide](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/formats.md#custom-formats-experimental) about them.
* There are also existing skeleton implementations that you may find useful: [AbstractEncoder] and [AbstractDecoder].
*/
@RequiresOptIn(
"You should implement Encoder or Decoder only if you want to write a custom kotlinx.serialization format. " +
"Before doing so, please consult official guide at https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/formats.md#custom-formats-experimental",
level = RequiresOptIn.Level.WARNING
)
public annotation class AdvancedEncodingApi
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import kotlinx.serialization.modules.*
/**
* Encoder that does not do any operations. Its main purpose is to ignore data instead of writing it.
*/
@OptIn(ExperimentalSerializationApi::class)
@OptIn(ExperimentalSerializationApi::class, AdvancedEncodingApi::class)
internal object NoOpEncoder : AbstractEncoder() {
override val serializersModule: SerializersModule = EmptySerializersModule()

Expand Down
2 changes: 2 additions & 0 deletions core/commonMain/src/kotlinx/serialization/internal/Tagged.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import kotlinx.serialization.modules.*
* They neither do have stable API, nor internal invariants and are changed without any warnings.
*/
@InternalSerializationApi
@OptIn(AdvancedEncodingApi::class)
public abstract class TaggedEncoder<Tag : Any?> : Encoder, CompositeEncoder {

/**
Expand Down Expand Up @@ -176,6 +177,7 @@ public abstract class NamedValueEncoder : TaggedEncoder<String>() {
}

@InternalSerializationApi
@OptIn(AdvancedEncodingApi::class)
public abstract class TaggedDecoder<Tag : Any?> : Decoder, CompositeDecoder {
override val serializersModule: SerializersModule
get() = EmptySerializersModule()
Expand Down
15 changes: 14 additions & 1 deletion docs/formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,7 @@ import kotlinx.serialization.modules.*
-->

```kotlin
@OptIn(AdvancedEncodingApi::class)
class ListEncoder : AbstractEncoder() {
val list = mutableListOf<Any>()

Expand Down Expand Up @@ -814,6 +815,7 @@ import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.modules.*
@OptIn(AdvancedEncodingApi::class)
class ListEncoder : AbstractEncoder() {
val list = mutableListOf<Any>()
Expand Down Expand Up @@ -845,6 +847,7 @@ A decoder needs to implement more substance.
each structure that is being recursively decoded keeps track of its own `elementIndex` state separately.

```kotlin
@OptIn(AdvancedEncodingApi::class)
class ListDecoder(val list: ArrayDeque<Any>) : AbstractDecoder() {
private var elementIndex = 0

Expand Down Expand Up @@ -922,6 +925,7 @@ import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.modules.*
@OptIn(AdvancedEncodingApi::class)
class ListEncoder : AbstractEncoder() {
val list = mutableListOf<Any>()
Expand All @@ -942,6 +946,7 @@ inline fun <reified T> encodeToList(value: T) = encodeToList(serializer(), value
-->

```kotlin
@OptIn(AdvancedEncodingApi::class)
class ListDecoder(val list: ArrayDeque<Any>) : AbstractDecoder() {
private var elementIndex = 0

Expand Down Expand Up @@ -1009,6 +1014,7 @@ import kotlinx.serialization.modules.*
-->

```kotlin
@OptIn(AdvancedEncodingApi::class)
class ListEncoder : AbstractEncoder() {
val list = mutableListOf<Any>()

Expand Down Expand Up @@ -1042,6 +1048,7 @@ in addition to the previous code.
> The formats that store collection size in advance have to return `true` from `decodeSequentially`.
```kotlin
@OptIn(AdvancedEncodingApi::class)
class ListDecoder(val list: ArrayDeque<Any>, var elementsCount: Int = 0) : AbstractDecoder() {
private var elementIndex = 0

Expand Down Expand Up @@ -1114,6 +1121,7 @@ import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.modules.*
@OptIn(AdvancedEncodingApi::class)
class ListEncoder : AbstractEncoder() {
val list = mutableListOf<Any>()
Expand Down Expand Up @@ -1147,6 +1155,7 @@ fun <T> encodeToList(serializer: SerializationStrategy<T>, value: T): List<Any>
inline fun <reified T> encodeToList(value: T) = encodeToList(serializer(), value)
@OptIn(AdvancedEncodingApi::class)
class ListDecoder(val list: ArrayDeque<Any>, var elementsCount: Int = 0) : AbstractDecoder() {
private var elementIndex = 0
Expand Down Expand Up @@ -1230,7 +1239,8 @@ import kotlinx.serialization.modules.*
import java.io.*
-->

```kotlin
```kotlin
@OptIn(AdvancedEncodingApi::class)
class DataOutputEncoder(val output: DataOutput) : AbstractEncoder() {
override val serializersModule: SerializersModule = EmptySerializersModule()
override fun encodeBoolean(value: Boolean) = output.writeByte(if (value) 1 else 0)
Expand Down Expand Up @@ -1267,6 +1277,7 @@ inline fun <reified T> encodeTo(output: DataOutput, value: T) = encodeTo(output,
The decoder implementation mirrors encoder's implementation overriding all the primitive `decodeXxx` functions.

```kotlin
@OptIn(AdvancedEncodingApi::class)
class DataInputDecoder(val input: DataInput, var elementsCount: Int = 0) : AbstractDecoder() {
private var elementIndex = 0
override val serializersModule: SerializersModule = EmptySerializersModule()
Expand Down Expand Up @@ -1383,6 +1394,7 @@ we add a trivial implementation of `encodeCompactSize` function that uses only o
a size of up to 254 bytes.

<!--- INCLUDE
@OptIn(AdvancedEncodingApi::class)
class DataOutputEncoder(val output: DataOutput) : AbstractEncoder() {
override val serializersModule: SerializersModule = EmptySerializersModule()
override fun encodeBoolean(value: Boolean) = output.writeByte(if (value) 1 else 0)
Expand Down Expand Up @@ -1438,6 +1450,7 @@ fun <T> encodeTo(output: DataOutput, serializer: SerializationStrategy<T>, value
inline fun <reified T> encodeTo(output: DataOutput, value: T) = encodeTo(output, serializer(), value)
@OptIn(AdvancedEncodingApi::class)
class DataInputDecoder(val input: DataInput, var elementsCount: Int = 0) : AbstractDecoder() {
private var elementIndex = 0
override val serializersModule: SerializersModule = EmptySerializersModule()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import kotlinx.serialization.encoding.*
* ```
*/
@ExperimentalSerializationApi
@OptIn(AdvancedEncodingApi::class)
@SubclassOptInRequired(SealedSerializationApi::class)
public interface CborDecoder : Decoder {
/**
* Exposes the current [Cbor] instance and all its configuration flags. Useful for low-level custom serializers.
*/
public val cbor: Cbor
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import kotlinx.serialization.encoding.*
* ```
*/
@ExperimentalSerializationApi
@OptIn(AdvancedEncodingApi::class)
@SubclassOptInRequired(SealedSerializationApi::class)
public interface CborEncoder : Encoder {
/**
* Exposes the current [Cbor] instance and all its configuration flags. Useful for low-level custom serializers.
*/
public val cbor: Cbor
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.modules.*

@OptIn(AdvancedEncodingApi::class)
internal open class CborReader(override val cbor: Cbor, protected val parser: CborParser) : AbstractDecoder(),
CborDecoder {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ private fun Stack.peek() = last()

// Writes class as map [fieldName, fieldValue]
// Split implementation to optimize base case
@OptIn(AdvancedEncodingApi::class)
internal sealed class CborWriter(
override val cbor: Cbor,
protected val output: ByteArrayOutput,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import kotlinx.serialization.descriptors.*
* Accepting this interface in your API methods, casting [Decoder] to [JsonDecoder] and invoking its
* methods is considered stable.
*/
@OptIn(AdvancedEncodingApi::class)
@SubclassOptInRequired(SealedSerializationApi::class)
public interface JsonDecoder : Decoder, CompositeDecoder {
/**
* An instance of the current [Json].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package kotlinx.serialization.json

import kotlinx.serialization.*
import kotlinx.serialization.encoding.*

/**
Expand Down Expand Up @@ -49,6 +50,8 @@ import kotlinx.serialization.encoding.*
* Accepting this interface in your API methods, casting [Encoder] to [JsonEncoder] and invoking its
* methods is considered stable.
*/
@OptIn(AdvancedEncodingApi::class)
@SubclassOptInRequired(SealedSerializationApi::class)
public interface JsonEncoder : Encoder, CompositeEncoder {
/**
* An instance of the current [Json].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import kotlin.jvm.*
/**
* [JsonDecoder] which reads given JSON from [AbstractJsonLexer] field by field.
*/
@OptIn(ExperimentalSerializationApi::class)
@OptIn(ExperimentalSerializationApi::class, AdvancedEncodingApi::class)
internal open class StreamingJsonDecoder(
final override val json: Json,
private val mode: WriteMode,
Expand Down Expand Up @@ -366,7 +366,7 @@ public fun <T> decodeStringToJsonTree(
return tree
}

@OptIn(ExperimentalSerializationApi::class)
@OptIn(ExperimentalSerializationApi::class, AdvancedEncodingApi::class)
internal class JsonDecoderForUnsignedTypes(
private val lexer: AbstractJsonLexer,
json: Json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal val SerialDescriptor.isUnsignedNumber: Boolean
internal val SerialDescriptor.isUnquotedLiteral: Boolean
get() = this.isInline && this == jsonUnquotedLiteralDescriptor

@OptIn(ExperimentalSerializationApi::class)
@OptIn(ExperimentalSerializationApi::class, AdvancedEncodingApi::class)
internal class StreamingJsonEncoder(
private val composer: Composer,
override val json: Json,
Expand Down
Loading

0 comments on commit 3c7da48

Please sign in to comment.