Skip to content

Commit 3c7da48

Browse files
committed
Introduce @AdvancedEncodingApi and @SealedSerializationApi annotations
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
1 parent 0b3288f commit 3c7da48

35 files changed

+115
-11
lines changed

build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ val excludedFromBomProjects get() = unpublishedProjects + "kotlinx-serialization
175175
val experimentalsEnabled get() = listOf(
176176
"-progressive",
177177
"-opt-in=kotlin.ExperimentalMultiplatform",
178+
"-opt-in=kotlin.ExperimentalSubclassOptIn",
178179
"-opt-in=kotlinx.serialization.InternalSerializationApi",
179180
"-P", "plugin:org.jetbrains.kotlinx.serialization:disableIntrinsic=false"
180181
)

buildSrc/src/main/kotlin/source-sets-conventions.gradle.kts

+4
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,10 @@ kotlin {
6969
progressiveMode = true
7070

7171
optIn("kotlin.ExperimentalMultiplatform")
72+
optIn("kotlin.ExperimentalSubclassOptIn")
7273
optIn("kotlin.ExperimentalStdlibApi")
7374
optIn("kotlinx.serialization.InternalSerializationApi")
75+
optIn("kotlinx.serialization.SealedSerializationApi")
7476
}
7577
}
7678

@@ -151,7 +153,9 @@ kotlin {
151153
sourceSets.matching({ it.name.contains("Test") }).configureEach {
152154
languageSettings {
153155
optIn("kotlinx.serialization.InternalSerializationApi")
156+
optIn("kotlinx.serialization.SealedSerializationApi")
154157
optIn("kotlinx.serialization.ExperimentalSerializationApi")
158+
optIn("kotlinx.serialization.encoding.AdvancedEncodingApi")
155159
}
156160
}
157161

core/api/kotlinx-serialization-core.api

+6
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ public final class kotlinx/serialization/SealedClassSerializer : kotlinx/seriali
8686
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
8787
}
8888

89+
public abstract interface annotation class kotlinx/serialization/SealedSerializationApi : java/lang/annotation/Annotation {
90+
}
91+
8992
public abstract interface class kotlinx/serialization/SerialFormat {
9093
public abstract fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
9194
}
@@ -423,6 +426,9 @@ public abstract class kotlinx/serialization/encoding/AbstractEncoder : kotlinx/s
423426
public fun shouldEncodeElementDefault (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Z
424427
}
425428

429+
public abstract interface annotation class kotlinx/serialization/encoding/AdvancedEncodingApi : java/lang/annotation/Annotation {
430+
}
431+
426432
public abstract interface class kotlinx/serialization/encoding/ChunkedDecoder {
427433
public abstract fun decodeStringChunked (Lkotlin/jvm/functions/Function1;)V
428434
}

core/api/kotlinx-serialization-core.klib.api

+8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
// - Show declarations: true
88

99
// Library unique name: <org.jetbrains.kotlinx:kotlinx-serialization-core>
10+
open annotation class kotlinx.serialization.encoding/AdvancedEncodingApi : kotlin/Annotation { // kotlinx.serialization.encoding/AdvancedEncodingApi|null[0]
11+
constructor <init>() // kotlinx.serialization.encoding/AdvancedEncodingApi.<init>|<init>(){}[0]
12+
}
13+
1014
open annotation class kotlinx.serialization.internal/NamedCompanion : kotlin/Annotation { // kotlinx.serialization.internal/NamedCompanion|null[0]
1115
constructor <init>() // kotlinx.serialization.internal/NamedCompanion.<init>|<init>(){}[0]
1216
}
@@ -61,6 +65,10 @@ open annotation class kotlinx.serialization/Required : kotlin/Annotation { // ko
6165
constructor <init>() // kotlinx.serialization/Required.<init>|<init>(){}[0]
6266
}
6367

68+
open annotation class kotlinx.serialization/SealedSerializationApi : kotlin/Annotation { // kotlinx.serialization/SealedSerializationApi|null[0]
69+
constructor <init>() // kotlinx.serialization/SealedSerializationApi.<init>|<init>(){}[0]
70+
}
71+
6472
open annotation class kotlinx.serialization/SerialInfo : kotlin/Annotation { // kotlinx.serialization/SerialInfo|null[0]
6573
constructor <init>() // kotlinx.serialization/SerialInfo.<init>|<init>(){}[0]
6674
}

core/commonMain/src/kotlinx/serialization/Annotations.kt

+14
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,17 @@ public annotation class ExperimentalSerializationApi
376376
@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.TYPEALIAS)
377377
@RequiresOptIn(level = RequiresOptIn.Level.ERROR)
378378
public annotation class InternalSerializationApi
379+
380+
/**
381+
* Marks interfaces and non-final classes that can be freely referenced in users' code but should not be
382+
* implemented or inherited. Such declarations are effectively `sealed` and do not have this modifier purely for technical reasons.
383+
*
384+
* kotlinx.serialization library provides compatibility guarantees for existing signatures of such classes;
385+
* however, new functions or properties can be added to them in any release.
386+
*/
387+
@MustBeDocumented
388+
@Target(AnnotationTarget.CLASS)
389+
@RequiresOptIn(message = "This class or interface should not be inherited/implemented outside of kotlinx.serialization library. " +
390+
"Note it is still permitted to use it directly. Read its documentation about inheritance for details.", level = RequiresOptIn.Level.ERROR)
391+
public annotation class SealedSerializationApi
392+

core/commonMain/src/kotlinx/serialization/descriptors/SerialDescriptor.kt

+2
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ import kotlinx.serialization.encoding.*
142142
* might be added to this interface when kotlinx.serialization adds support for new Kotlin features.
143143
* This interface is safe to use and construct via [buildClassSerialDescriptor], [PrimitiveSerialDescriptor], and `SerialDescriptor` factory function.
144144
*/
145+
// TODO: there was a phrase 'and is safe to delegate implementation to existing instances.' but it is not true unless we enable -Xjvm-default
146+
@SubclassOptInRequired(SealedSerializationApi::class)
145147
public interface SerialDescriptor {
146148
/**
147149
* Serial name of the descriptor that identifies a pair of the associated serializer and target class.

core/commonMain/src/kotlinx/serialization/encoding/AbstractDecoder.kt

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import kotlinx.serialization.descriptors.*
1414
* See [Decoder] documentation for information about each particular `decode*` method.
1515
*/
1616
@ExperimentalSerializationApi
17+
@SubclassOptInRequired(AdvancedEncodingApi::class)
1718
public abstract class AbstractDecoder : Decoder, CompositeDecoder {
1819

1920
/**

core/commonMain/src/kotlinx/serialization/encoding/AbstractEncoder.kt

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import kotlinx.serialization.internal.*
1616
* See [Encoder] documentation for information about each particular `encode*` method.
1717
*/
1818
@ExperimentalSerializationApi
19+
@SubclassOptInRequired(AdvancedEncodingApi::class)
1920
public abstract class AbstractEncoder : Encoder, CompositeEncoder {
2021

2122
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder = this

core/commonMain/src/kotlinx/serialization/encoding/Decoding.kt

+2
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ import kotlinx.serialization.modules.*
111111
* `Decoder` interface is not stable for inheritance in 3rd-party libraries, as new methods
112112
* might be added to this interface or contracts of the existing methods can be changed.
113113
*/
114+
@SubclassOptInRequired(AdvancedEncodingApi::class)
114115
public interface Decoder {
115116
/**
116117
* Context of the current serialization process, including contextual and polymorphic serialization and,
@@ -292,6 +293,7 @@ internal inline fun <T : Any> Decoder.decodeIfNullable(deserializer: Deserializa
292293
* `CompositeDecoder` interface is not stable for inheritance in 3rd party libraries, as new methods
293294
* might be added to this interface or contracts of the existing methods can be changed.
294295
*/
296+
@SubclassOptInRequired(AdvancedEncodingApi::class)
295297
public interface CompositeDecoder {
296298

297299
/**

core/commonMain/src/kotlinx/serialization/encoding/Encoding.kt

+2
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ import kotlinx.serialization.modules.*
107107
* `Encoder` interface is not stable for inheritance in 3rd party libraries, as new methods
108108
* might be added to this interface or contracts of the existing methods can be changed.
109109
*/
110+
@SubclassOptInRequired(AdvancedEncodingApi::class)
110111
public interface Encoder {
111112
/**
112113
* Context of the current serialization process, including contextual and polymorphic serialization and,
@@ -320,6 +321,7 @@ public interface Encoder {
320321
* `CompositeEncoder` interface is not stable for inheritance in 3rd party libraries, as new methods
321322
* might be added to this interface or contracts of the existing methods can be changed.
322323
*/
324+
@SubclassOptInRequired(AdvancedEncodingApi::class)
323325
public interface CompositeEncoder {
324326
/**
325327
* Context of the current serialization process, including contextual and polymorphic serialization and,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.encoding
6+
7+
/**
8+
* Marks all encoding- and decoding-related interfaces in kotlinx.serialization.
9+
* These interfaces are used in serializers and have to be implemented only if you want to write
10+
* a custom serialization format. Since encoder/decoder invariants are quite complex,
11+
* it is recommended to start with reading their documentation: see [Encoder] and [Decoder],
12+
* and [kotlinx.serialization guide](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/formats.md#custom-formats-experimental) about them.
13+
* There are also existing skeleton implementations that you may find useful: [AbstractEncoder] and [AbstractDecoder].
14+
*/
15+
@RequiresOptIn(
16+
"You should implement Encoder or Decoder only if you want to write a custom kotlinx.serialization format. " +
17+
"Before doing so, please consult official guide at https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/formats.md#custom-formats-experimental",
18+
level = RequiresOptIn.Level.WARNING
19+
)
20+
public annotation class AdvancedEncodingApi

core/commonMain/src/kotlinx/serialization/internal/NoOpEncoder.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import kotlinx.serialization.modules.*
1212
/**
1313
* Encoder that does not do any operations. Its main purpose is to ignore data instead of writing it.
1414
*/
15-
@OptIn(ExperimentalSerializationApi::class)
15+
@OptIn(ExperimentalSerializationApi::class, AdvancedEncodingApi::class)
1616
internal object NoOpEncoder : AbstractEncoder() {
1717
override val serializersModule: SerializersModule = EmptySerializersModule()
1818

core/commonMain/src/kotlinx/serialization/internal/Tagged.kt

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import kotlinx.serialization.modules.*
1515
* They neither do have stable API, nor internal invariants and are changed without any warnings.
1616
*/
1717
@InternalSerializationApi
18+
@OptIn(AdvancedEncodingApi::class)
1819
public abstract class TaggedEncoder<Tag : Any?> : Encoder, CompositeEncoder {
1920

2021
/**
@@ -176,6 +177,7 @@ public abstract class NamedValueEncoder : TaggedEncoder<String>() {
176177
}
177178

178179
@InternalSerializationApi
180+
@OptIn(AdvancedEncodingApi::class)
179181
public abstract class TaggedDecoder<Tag : Any?> : Decoder, CompositeDecoder {
180182
override val serializersModule: SerializersModule
181183
get() = EmptySerializersModule()

docs/formats.md

+14-1
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,7 @@ import kotlinx.serialization.modules.*
747747
-->
748748

749749
```kotlin
750+
@OptIn(AdvancedEncodingApi::class)
750751
class ListEncoder : AbstractEncoder() {
751752
val list = mutableListOf<Any>()
752753

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

847849
```kotlin
850+
@OptIn(AdvancedEncodingApi::class)
848851
class ListDecoder(val list: ArrayDeque<Any>) : AbstractDecoder() {
849852
private var elementIndex = 0
850853

@@ -922,6 +925,7 @@ import kotlinx.serialization.descriptors.*
922925
import kotlinx.serialization.encoding.*
923926
import kotlinx.serialization.modules.*
924927
928+
@OptIn(AdvancedEncodingApi::class)
925929
class ListEncoder : AbstractEncoder() {
926930
val list = mutableListOf<Any>()
927931
@@ -942,6 +946,7 @@ inline fun <reified T> encodeToList(value: T) = encodeToList(serializer(), value
942946
-->
943947

944948
```kotlin
949+
@OptIn(AdvancedEncodingApi::class)
945950
class ListDecoder(val list: ArrayDeque<Any>) : AbstractDecoder() {
946951
private var elementIndex = 0
947952

@@ -1009,6 +1014,7 @@ import kotlinx.serialization.modules.*
10091014
-->
10101015

10111016
```kotlin
1017+
@OptIn(AdvancedEncodingApi::class)
10121018
class ListEncoder : AbstractEncoder() {
10131019
val list = mutableListOf<Any>()
10141020

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

@@ -1114,6 +1121,7 @@ import kotlinx.serialization.descriptors.*
11141121
import kotlinx.serialization.encoding.*
11151122
import kotlinx.serialization.modules.*
11161123
1124+
@OptIn(AdvancedEncodingApi::class)
11171125
class ListEncoder : AbstractEncoder() {
11181126
val list = mutableListOf<Any>()
11191127
@@ -1147,6 +1155,7 @@ fun <T> encodeToList(serializer: SerializationStrategy<T>, value: T): List<Any>
11471155
11481156
inline fun <reified T> encodeToList(value: T) = encodeToList(serializer(), value)
11491157
1158+
@OptIn(AdvancedEncodingApi::class)
11501159
class ListDecoder(val list: ArrayDeque<Any>, var elementsCount: Int = 0) : AbstractDecoder() {
11511160
private var elementIndex = 0
11521161
@@ -1230,7 +1239,8 @@ import kotlinx.serialization.modules.*
12301239
import java.io.*
12311240
-->
12321241

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

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

13851396
<!--- INCLUDE
1397+
@OptIn(AdvancedEncodingApi::class)
13861398
class DataOutputEncoder(val output: DataOutput) : AbstractEncoder() {
13871399
override val serializersModule: SerializersModule = EmptySerializersModule()
13881400
override fun encodeBoolean(value: Boolean) = output.writeByte(if (value) 1 else 0)
@@ -1438,6 +1450,7 @@ fun <T> encodeTo(output: DataOutput, serializer: SerializationStrategy<T>, value
14381450
14391451
inline fun <reified T> encodeTo(output: DataOutput, value: T) = encodeTo(output, serializer(), value)
14401452
1453+
@OptIn(AdvancedEncodingApi::class)
14411454
class DataInputDecoder(val input: DataInput, var elementsCount: Int = 0) : AbstractDecoder() {
14421455
private var elementIndex = 0
14431456
override val serializersModule: SerializersModule = EmptySerializersModule()

formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborDecoder.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ import kotlinx.serialization.encoding.*
2525
* ```
2626
*/
2727
@ExperimentalSerializationApi
28+
@OptIn(AdvancedEncodingApi::class)
29+
@SubclassOptInRequired(SealedSerializationApi::class)
2830
public interface CborDecoder : Decoder {
2931
/**
3032
* Exposes the current [Cbor] instance and all its configuration flags. Useful for low-level custom serializers.
3133
*/
3234
public val cbor: Cbor
33-
}
35+
}

formats/cbor/commonMain/src/kotlinx/serialization/cbor/CborEncoder.kt

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ import kotlinx.serialization.encoding.*
2525
* ```
2626
*/
2727
@ExperimentalSerializationApi
28+
@OptIn(AdvancedEncodingApi::class)
29+
@SubclassOptInRequired(SealedSerializationApi::class)
2830
public interface CborEncoder : Encoder {
2931
/**
3032
* Exposes the current [Cbor] instance and all its configuration flags. Useful for low-level custom serializers.
3133
*/
3234
public val cbor: Cbor
33-
}
35+
}

formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Decoder.kt

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import kotlinx.serialization.descriptors.*
1212
import kotlinx.serialization.encoding.*
1313
import kotlinx.serialization.modules.*
1414

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

formats/cbor/commonMain/src/kotlinx/serialization/cbor/internal/Encoder.kt

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ private fun Stack.peek() = last()
2424

2525
// Writes class as map [fieldName, fieldValue]
2626
// Split implementation to optimize base case
27+
@OptIn(AdvancedEncodingApi::class)
2728
internal sealed class CborWriter(
2829
override val cbor: Cbor,
2930
protected val output: ByteArrayOutput,

formats/json/commonMain/src/kotlinx/serialization/json/JsonDecoder.kt

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import kotlinx.serialization.descriptors.*
5151
* Accepting this interface in your API methods, casting [Decoder] to [JsonDecoder] and invoking its
5252
* methods is considered stable.
5353
*/
54+
@OptIn(AdvancedEncodingApi::class)
55+
@SubclassOptInRequired(SealedSerializationApi::class)
5456
public interface JsonDecoder : Decoder, CompositeDecoder {
5557
/**
5658
* An instance of the current [Json].

formats/json/commonMain/src/kotlinx/serialization/json/JsonEncoder.kt

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package kotlinx.serialization.json
66

7+
import kotlinx.serialization.*
78
import kotlinx.serialization.encoding.*
89

910
/**
@@ -49,6 +50,8 @@ import kotlinx.serialization.encoding.*
4950
* Accepting this interface in your API methods, casting [Encoder] to [JsonEncoder] and invoking its
5051
* methods is considered stable.
5152
*/
53+
@OptIn(AdvancedEncodingApi::class)
54+
@SubclassOptInRequired(SealedSerializationApi::class)
5255
public interface JsonEncoder : Encoder, CompositeEncoder {
5356
/**
5457
* An instance of the current [Json].

formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonDecoder.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import kotlin.jvm.*
1717
/**
1818
* [JsonDecoder] which reads given JSON from [AbstractJsonLexer] field by field.
1919
*/
20-
@OptIn(ExperimentalSerializationApi::class)
20+
@OptIn(ExperimentalSerializationApi::class, AdvancedEncodingApi::class)
2121
internal open class StreamingJsonDecoder(
2222
final override val json: Json,
2323
private val mode: WriteMode,
@@ -366,7 +366,7 @@ public fun <T> decodeStringToJsonTree(
366366
return tree
367367
}
368368

369-
@OptIn(ExperimentalSerializationApi::class)
369+
@OptIn(ExperimentalSerializationApi::class, AdvancedEncodingApi::class)
370370
internal class JsonDecoderForUnsignedTypes(
371371
private val lexer: AbstractJsonLexer,
372372
json: Json

formats/json/commonMain/src/kotlinx/serialization/json/internal/StreamingJsonEncoder.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ internal val SerialDescriptor.isUnsignedNumber: Boolean
2424
internal val SerialDescriptor.isUnquotedLiteral: Boolean
2525
get() = this.isInline && this == jsonUnquotedLiteralDescriptor
2626

27-
@OptIn(ExperimentalSerializationApi::class)
27+
@OptIn(ExperimentalSerializationApi::class, AdvancedEncodingApi::class)
2828
internal class StreamingJsonEncoder(
2929
private val composer: Composer,
3030
override val json: Json,

0 commit comments

Comments
 (0)