diff options
author | Vsevolod Tolstopyatov <qwwdfsad@gmail.com> | 2022-07-04 12:17:24 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-04 13:17:24 +0300 |
commit | 0f1034e670a6581cdba54e4aa5cb1257e5884ea9 (patch) | |
tree | 33460ec178a905fa3c77ee874a3241eb4ff1ea39 | |
parent | 621dbf8992e8ff508e8a265e4733d7893a9aeab8 (diff) | |
download | kotlinx.serialization-0f1034e670a6581cdba54e4aa5cb1257e5884ea9.tar.gz |
Update value classes guide (#1973)
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | docs/inline-classes.md | 206 | ||||
-rw-r--r-- | docs/serialization-guide.md | 10 | ||||
-rw-r--r-- | docs/value-classes.md | 204 | ||||
-rw-r--r-- | formats/json/commonMain/src/kotlinx/serialization/json/Json.kt | 1 |
5 files changed, 211 insertions, 212 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 621bf09e..2025e1a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -297,7 +297,7 @@ Using 1.1.0-RC, you can mark inline classes as `@Serializable` and use them in o Unsigned integer types (`UByte`, `UShort`, `UInt` and `ULong`) are serializable as well and have special support in JSON. This feature requires Kotlin compiler 1.4.30-RC and enabling new IR compilers for [JS](https://kotlinlang.org/docs/reference/js-ir-compiler.html) and [JVM](https://kotlinlang.org/docs/reference/whatsnew14.html#new-jvm-ir-backend). -You can learn more in the [documentation](docs/inline-classes.md) +You can learn more in the [documentation](docs/value-classes.md) and corresponding [pull request](https://github.com/Kotlin/kotlinx.serialization/pull/1244). ### Other features diff --git a/docs/inline-classes.md b/docs/inline-classes.md index a28f51c0..fda81420 100644 --- a/docs/inline-classes.md +++ b/docs/inline-classes.md @@ -1,205 +1 @@ -# Serialization and inline classes (experimental, IR-specific) - -This appendix describes how inline classes are handled by kotlinx.serialization. - -> Features described in this document are currently [experimental](https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/compatibility.md#experimental-api) -> and are available only with IR compilers. Native targets use IR compiler by default; -> see documentation for [JS](https://kotlinlang.org/docs/reference/js-ir-compiler.html) and [JVM](https://kotlinlang.org/docs/reference/whatsnew14.html#new-jvm-ir-backend) to learn how to enable IR compilers. -> Inline classes themselves are an [Alpha](https://kotlinlang.org/docs/reference/inline-classes.html#alpha-status-of-inline-classes) Kotlin feature. - -**Table of contents** - -<!--- TOC --> - -* [Serializable inline classes](#serializable-inline-classes) -* [Unsigned types support (JSON only)](#unsigned-types-support-json-only) -* [Using inline classes in your custom serializers](#using-inline-classes-in-your-custom-serializers) - -<!--- END --> - -## Serializable inline classes - -We can mark inline class as serializable: - -```kotlin -@Serializable -inline class Color(val rgb: Int) -``` - -Inline class in Kotlin is stored as its underlying type when possible (i.e. no boxing is required). -Serialization framework makes does not impose any additional restriction and uses the underlying type where possible as well. - -```kotlin -@Serializable -data class NamedColor(val color: Color, val name: String) - -fun main() { - println(Json.encodeToString(NamedColor(Color(0), "black"))) -} -``` - -In this example, `NamedColor` is serialized as two primitives: `color: Int` and `name: String` without an allocation -of `Color` class. When we run the example, encoding data with JSON format, we get the following -output: - -```text -{"color": 0, "name": "black"} -``` - -As we see, `Color` class is not included during the encoding, only its underlying data. This invariant holds even if the actual inline class -is [allocated](https://kotlinlang.org/docs/reference/inline-classes.html#representation) — for example, when inline -class is used as a generic type argument: - -```kotlin -@Serializable -class Palette(val colors: List<Color>) - -fun main() { - println(Json.encodeToString(Palette(listOf(Color(0), Color(255), Color(128))))) -} -``` - -The snippet produces the following output: - -```text -{"colors":[0, 255, 128]} -``` - -## Unsigned types support (JSON only) - -Kotlin standard library provides ready-to-use unsigned arithmetics, leveraging inline classes -to represent unsigned types: `UByte`, `UShort`, `UInt` and `ULong`. -[Json] format has built-in support for them: these types are serialized as theirs string -representations in unsigned form. -These types are handled as regular serializable types by the compiler plugin and can be freely used in serializable classes: - -```kotlin -@Serializable -class Counter(val counted: UByte, val description: String) - -fun main() { - val counted = 239.toUByte() - println(Json.encodeToString(Counter(counted, "tries"))) -} -``` - -The output is following: - -```text -{"counted":239,"description":"tries"} -``` - -> Unsigned types are currently unsupported in Protobuf and CBOR, but we plan to add them later. - -## Using inline classes in your custom serializers - -Let's return to our `NamedColor` example and try to write a custom serializer for it. Normally, as shown -in [Hand-written composite serializer](serializers.md#hand-written-composite-serializer), we would write the following code -in `serialize` method: - -```kotlin -override fun serialize(encoder: Encoder, value: NamedColor) { - encoder.beginStructure(descriptor) { - encodeSerializableElement(descriptor, 0, Color.serializer(), value.color) - encodeStringElement(descriptor, 1, value.name) - } -} -``` - -However, since `Color` is used as a type argument in [encodeSerializableElement][CompositeEncoder.encodeSerializableElement] function, `value.color` will be boxed -to `Color` wrapper before passing it to the function, preventing the inline class optimization. To avoid this, we can use -special [encodeInlineElement][CompositeEncoder.encodeInlineElement] function instead. It uses [serial descriptor][SerialDescriptor] of `Color` ([retrieved][SerialDescriptor.getElementDescriptor] from serial descriptor of `NamedColor`) instead of [KSerializer], -does not have type parameters and does not accept any values. Instead, it returns [Encoder]. Using it, we can encode -unboxed value: - -```kotlin -override fun serialize(encoder: Encoder, value: NamedColor) { - encoder.beginStructure(descriptor) { - encodeInlineElement(descriptor, 0).encodeInt(value.color) - encodeStringElement(descriptor, 1, value.name) - } -} -``` - -The same principle goes also with [CompositeDecoder]: it has [decodeInlineElement][CompositeDecoder.decodeInlineElement] function that returns [Decoder]. - -If your class should be represented as a primitive (as shown in [Primitive serializer](serializers.md#primitive-serializer) section), -and you cannot use [beginStructure][Encoder.beginStructure] function, there is a complementary function in [Encoder] called [encodeInline][Encoder.encodeInline]. -We will use it to show an example how one can represent a class as an unsigned integer. - -Let's start with a UID class: - -```kotlin -@Serializable(UIDSerializer::class) -class UID(val uid: Int) -``` - -`uid` type is `Int`, but suppose we want it to be an unsigned integer in JSON. We can start writing the -following custom serializer: - -```kotlin -object UIDSerializer: KSerializer<UID> { - override val descriptor = UInt.serializer().descriptor -} -``` - -Note that we are using here descriptor from `UInt.serializer()` — it means that the class' representation looks like a -UInt's one. - -Then the `serialize` method: - -```kotlin -override fun serialize(encoder: Encoder, value: UID) { - encoder.encodeInline(descriptor).encodeInt(value.uid) -} -``` - -That's where the magic happens — despite we called a regular [encodeInt][Encoder.encodeInt] with a `uid: Int` argument, the output will contain -an unsigned int because of the special encoder from `encodeInline` function. Since JSON format supports unsigned integers, it -recognizes theirs descriptors when they're passed into `encodeInline` and handles consecutive calls as for unsigned integers. - -The `deserialize` method looks symmetrically: - -```kotlin -override fun deserialize(decoder: Decoder): UID { - return UID(decoder.decodeInline(descriptor).decodeInt()) -} -``` - -> Disclaimer: You can also write such a serializer for inline class itself (imagine UID being the inline class — there's no need to change anything in the serializer). -> However, do not use anything in custom serializers for inline classes besides `encodeInline`. As we discussed, calls to inline class serializer may be -> optimized and replaced with a `encodeInlineElement` calls. -> `encodeInline` and `encodeInlineElement` calls with the same descriptor are considered equivalent and can be replaced with each other — formats should return the same `Encoder`. -> If you embed custom logic in custom inline class serializer, you may get different results depending on whether this serializer was called at all -> (and this, in turn, depends on whether inline class was boxed or not). - ---- - -<!--- MODULE /kotlinx-serialization-core --> -<!--- INDEX kotlinx-serialization-core/kotlinx.serialization --> - -[KSerializer]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-k-serializer/index.html - -<!--- INDEX kotlinx-serialization-core/kotlinx.serialization.encoding --> - -[CompositeEncoder.encodeSerializableElement]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-composite-encoder/encode-serializable-element.html -[CompositeEncoder.encodeInlineElement]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-composite-encoder/encode-inline-element.html -[Encoder]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/index.html -[CompositeDecoder]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-composite-decoder/index.html -[CompositeDecoder.decodeInlineElement]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-composite-decoder/decode-inline-element.html -[Decoder]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-decoder/index.html -[Encoder.beginStructure]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/begin-structure.html -[Encoder.encodeInline]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/encode-inline.html -[Encoder.encodeInt]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/encode-int.html - -<!--- INDEX kotlinx-serialization-core/kotlinx.serialization.descriptors --> - -[SerialDescriptor]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.descriptors/-serial-descriptor/index.html -[SerialDescriptor.getElementDescriptor]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.descriptors/-serial-descriptor/get-element-descriptor.html - -<!--- MODULE /kotlinx-serialization-json --> -<!--- INDEX kotlinx-serialization-json/kotlinx.serialization.json --> - -[Json]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/index.html - -<!--- END --> +The documentation has been moved to the [value-classes.md](value-classes.md) page. diff --git a/docs/serialization-guide.md b/docs/serialization-guide.md index e3f28eae..e82e4dcc 100644 --- a/docs/serialization-guide.md +++ b/docs/serialization-guide.md @@ -153,10 +153,10 @@ Once the project is set up, we can start serializing some classes. * <a name='format-specific-types'></a>[Format-specific types](formats.md#format-specific-types) <!--- END --> -**Appendix A.** [Serialization and inline classes (experimental, IR-specific)](inline-classes.md) +**Appendix A.** [Serialization and value classes (IR-only)](value-classes.md) -<!--- TOC_REF inline-classes.md --> -* <a name='serializable-inline-classes'></a>[Serializable inline classes](inline-classes.md#serializable-inline-classes) -* <a name='unsigned-types-support-json-only'></a>[Unsigned types support (JSON only)](inline-classes.md#unsigned-types-support-json-only) -* <a name='using-inline-classes-in-your-custom-serializers'></a>[Using inline classes in your custom serializers](inline-classes.md#using-inline-classes-in-your-custom-serializers) +<!--- TOC_REF value-classes.md --> +* <a name='serializable-value-classes'></a>[Serializable value classes](value-classes.md#serializable-value-classes) +* <a name='unsigned-types-support-json-only'></a>[Unsigned types support (JSON only)](value-classes.md#unsigned-types-support-json-only) +* <a name='using-value-classes-in-your-custom-serializers'></a>[Using value classes in your custom serializers](value-classes.md#using-value-classes-in-your-custom-serializers) <!--- END --> diff --git a/docs/value-classes.md b/docs/value-classes.md new file mode 100644 index 00000000..476b5db0 --- /dev/null +++ b/docs/value-classes.md @@ -0,0 +1,204 @@ +# Serialization and value classes (IR-specific) + +This appendix describes how value classes are handled by kotlinx.serialization. + +> Features described are available on JVM/IR (enabled by default), JS/IR and Native backends. + +**Table of contents** + +<!--- TOC --> + +* [Serializable value classes](#serializable-value-classes) +* [Unsigned types support (JSON only)](#unsigned-types-support-json-only) +* [Using value classes in your custom serializers](#using-value-classes-in-your-custom-serializers) + +<!--- END --> + +## Serializable value classes + +We can mark value class as serializable: + +```kotlin +@Serializable +@JvmInline +value class Color(val rgb: Int) +``` + +Value class in Kotlin is stored as its underlying type when possible (i.e. no boxing is required). +Serialization framework does not impose any additional restrictions and uses the underlying type where possible as well. + +```kotlin +@Serializable +data class NamedColor(val color: Color, val name: String) + +fun main() { + println(Json.encodeToString(NamedColor(Color(0), "black"))) +} +``` + +In this example, `NamedColor` is serialized as two primitives: `color: Int` and `name: String` without an allocation +of `Color` class. When we run the example, encoding data with JSON format, we get the following +output: + +```text +{"color": 0, "name": "black"} +``` + +As we see, `Color` class is not included during the encoding, only its underlying data. This invariant holds even if the actual value class +is [allocated](https://kotlinlang.org/docs/reference/inline-classes.html#representation) — for example, when inline +class is used as a generic type argument: + +```kotlin +@Serializable +class Palette(val colors: List<Color>) + +fun main() { + println(Json.encodeToString(Palette(listOf(Color(0), Color(255), Color(128))))) +} +``` + +The snippet produces the following output: + +```text +{"colors":[0, 255, 128]} +``` + +## Unsigned types support (JSON only) + +Kotlin standard library provides ready-to-use unsigned arithmetics, leveraging value classes +to represent unsigned types: `UByte`, `UShort`, `UInt` and `ULong`. +[Json] format has built-in support for them: these types are serialized as theirs string +representations in unsigned form. +These types are handled as regular serializable types by the compiler plugin and can be freely used in serializable classes: + +```kotlin +@Serializable +class Counter(val counted: UByte, val description: String) + +fun main() { + val counted = 239.toUByte() + println(Json.encodeToString(Counter(counted, "tries"))) +} +``` + +The output is following: + +```text +{"counted":239,"description":"tries"} +``` + +> Unsigned types are currently supported only in JSON format. Other formats such as ProtoBuf and CBOR +> use built-in serializers that use an underlying signed representation for unsigned types. + +## Using value classes in your custom serializers + +Let's return to our `NamedColor` example and try to write a custom serializer for it. Normally, as shown +in [Hand-written composite serializer](serializers.md#hand-written-composite-serializer), we would write the following code +in `serialize` method: + +```kotlin +override fun serialize(encoder: Encoder, value: NamedColor) { + encoder.beginStructure(descriptor) { + encodeSerializableElement(descriptor, 0, Color.serializer(), value.color) + encodeStringElement(descriptor, 1, value.name) + } +} +``` + +However, since `Color` is used as a type argument in [encodeSerializableElement][CompositeEncoder.encodeSerializableElement] function, `value.color` will be boxed +to `Color` wrapper before passing it to the function, preventing the value class optimization. To avoid this, we can use +special [encodeInlineElement][CompositeEncoder.encodeInlineElement] function instead. It uses [serial descriptor][SerialDescriptor] of `Color` ([retrieved][SerialDescriptor.getElementDescriptor] from serial descriptor of `NamedColor`) instead of [KSerializer], +does not have type parameters and does not accept any values. Instead, it returns [Encoder]. Using it, we can encode +unboxed value: + +```kotlin +override fun serialize(encoder: Encoder, value: NamedColor) { + encoder.beginStructure(descriptor) { + encodeInlineElement(descriptor, 0).encodeInt(value.color) + encodeStringElement(descriptor, 1, value.name) + } +} +``` + +The same principle goes also with [CompositeDecoder]: it has [decodeInlineElement][CompositeDecoder.decodeInlineElement] function that returns [Decoder]. + +If your class should be represented as a primitive (as shown in [Primitive serializer](serializers.md#primitive-serializer) section), +and you cannot use [beginStructure][Encoder.beginStructure] function, there is a complementary function in [Encoder] called [encodeInline][Encoder.encodeInline]. +We will use it to show an example how one can represent a class as an unsigned integer. + +Let's start with a UID class: + +```kotlin +@Serializable(UIDSerializer::class) +class UID(val uid: Int) +``` + +`uid` type is `Int`, but suppose we want it to be an unsigned integer in JSON. We can start writing the +following custom serializer: + +```kotlin +object UIDSerializer: KSerializer<UID> { + override val descriptor = UInt.serializer().descriptor +} +``` + +Note that we are using here descriptor from `UInt.serializer()` — it means that the class' representation looks like a +UInt's one. + +Then the `serialize` method: + +```kotlin +override fun serialize(encoder: Encoder, value: UID) { + encoder.encodeInline(descriptor).encodeInt(value.uid) +} +``` + +That's where the magic happens — despite we called a regular [encodeInt][Encoder.encodeInt] with a `uid: Int` argument, the output will contain +an unsigned int because of the special encoder from `encodeInline` function. Since JSON format supports unsigned integers, it +recognizes theirs descriptors when they're passed into `encodeInline` and handles consecutive calls as for unsigned integers. + +The `deserialize` method looks symmetrically: + +```kotlin +override fun deserialize(decoder: Decoder): UID { + return UID(decoder.decodeInline(descriptor).decodeInt()) +} +``` + +> Disclaimer: You can also write such a serializer for value class itself (imagine UID being the value class — there's no need to change anything in the serializer). +> However, do not use anything in custom serializers for value classes besides `encodeInline`. As we discussed, calls to value class serializer may be +> optimized and replaced with a `encodeInlineElement` calls. +> `encodeInline` and `encodeInlineElement` calls with the same descriptor are considered equivalent and can be replaced with each other — formats should return the same `Encoder`. +> If you embed custom logic in custom value class serializer, you may get different results depending on whether this serializer was called at all +> (and this, in turn, depends on whether value class was boxed or not). + +--- + +<!--- MODULE /kotlinx-serialization-core --> +<!--- INDEX kotlinx-serialization-core/kotlinx.serialization --> + +[KSerializer]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-k-serializer/index.html + +<!--- INDEX kotlinx-serialization-core/kotlinx.serialization.encoding --> + +[CompositeEncoder.encodeSerializableElement]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-composite-encoder/encode-serializable-element.html +[CompositeEncoder.encodeInlineElement]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-composite-encoder/encode-inline-element.html +[Encoder]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/index.html +[CompositeDecoder]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-composite-decoder/index.html +[CompositeDecoder.decodeInlineElement]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-composite-decoder/decode-inline-element.html +[Decoder]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-decoder/index.html +[Encoder.beginStructure]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/begin-structure.html +[Encoder.encodeInline]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/encode-inline.html +[Encoder.encodeInt]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/encode-int.html + +<!--- INDEX kotlinx-serialization-core/kotlinx.serialization.descriptors --> + +[SerialDescriptor]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.descriptors/-serial-descriptor/index.html +[SerialDescriptor.getElementDescriptor]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.descriptors/-serial-descriptor/get-element-descriptor.html + +<!--- MODULE /kotlinx-serialization-json --> +<!--- INDEX kotlinx-serialization-json/kotlinx.serialization.json --> + +[Json]: https://kotlin.github.io/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/index.html + +<!--- END --> diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt index abb9b4bc..04871511 100644 --- a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt +++ b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt @@ -50,7 +50,6 @@ import kotlin.native.concurrent.* * Json instance also exposes its [configuration] that can be used in custom serializers * that rely on [JsonDecoder] and [JsonEncoder] for customizable behaviour. */ -@OptIn(ExperimentalSerializationApi::class) public sealed class Json( public val configuration: JsonConfiguration, override val serializersModule: SerializersModule |