xref: /aosp_15_r20/external/kotlinx.serialization/docs/value-classes.md (revision 57b5a4a64c534cf7f27ac9427ceab07f3d8ed3d8)
1# Serialization and value classes (IR-specific)
2
3This appendix describes how value classes are handled by kotlinx.serialization.
4
5> Features described are available on JVM/IR (enabled by default), JS/IR and Native backends.
6
7**Table of contents**
8
9<!--- TOC -->
10
11* [Serializable value classes](#serializable-value-classes)
12* [Unsigned types support (JSON only)](#unsigned-types-support-json-only)
13* [Using value classes in your custom serializers](#using-value-classes-in-your-custom-serializers)
14
15<!--- END -->
16
17## Serializable value classes
18
19We can mark value class as serializable:
20
21```kotlin
22@Serializable
23@JvmInline
24value class Color(val rgb: Int)
25```
26
27Value class in Kotlin is stored as its underlying type when possible (i.e. no boxing is required).
28Serialization framework does not impose any additional restrictions and uses the underlying type where possible as well.
29
30```kotlin
31@Serializable
32data class NamedColor(val color: Color, val name: String)
33
34fun main() {
35  println(Json.encodeToString(NamedColor(Color(0), "black")))
36}
37```
38
39In this example, `NamedColor` is serialized as two primitives: `color: Int` and `name: String` without an allocation
40of `Color` class. When we run the example, encoding data with JSON format, we get the following
41output:
42
43```text
44{"color": 0, "name": "black"}
45```
46
47As we see, `Color` class is not included during the encoding, only its underlying data. This invariant holds even if the actual value class
48is [allocated](https://kotlinlang.org/docs/reference/inline-classes.html#representation) — for example, when value
49class is used as a generic type argument:
50
51```kotlin
52@Serializable
53class Palette(val colors: List<Color>)
54
55fun main() {
56  println(Json.encodeToString(Palette(listOf(Color(0), Color(255), Color(128)))))
57}
58```
59
60The snippet produces the following output:
61
62```text
63{"colors":[0, 255, 128]}
64```
65
66## Unsigned types support (JSON only)
67
68Kotlin standard library provides ready-to-use unsigned arithmetics, leveraging value classes
69to represent unsigned types: `UByte`, `UShort`, `UInt` and `ULong`.
70[Json] format has built-in support for them: these types are serialized as theirs string
71representations in unsigned form.
72These types are handled as regular serializable types by the compiler plugin and can be freely used in serializable classes:
73
74```kotlin
75@Serializable
76class Counter(val counted: UByte, val description: String)
77
78fun main() {
79    val counted = 239.toUByte()
80    println(Json.encodeToString(Counter(counted, "tries")))
81}
82```
83
84The output is following:
85
86```text
87{"counted":239,"description":"tries"}
88```
89
90> Unsigned types are currently supported only in JSON format. Other formats such as ProtoBuf and CBOR
91> use built-in serializers that use an underlying signed representation for unsigned types.
92
93## Using value classes in your custom serializers
94
95Let's return to our `NamedColor` example and try to write a custom serializer for it. Normally, as shown
96in [Hand-written composite serializer](serializers.md#hand-written-composite-serializer), we would write the following code
97in `serialize` method:
98
99```kotlin
100override fun serialize(encoder: Encoder, value: NamedColor) {
101  encoder.encodeStructure(descriptor) {
102    encodeSerializableElement(descriptor, 0, Color.serializer(), value.color)
103    encodeStringElement(descriptor, 1, value.name)
104  }
105}
106```
107
108However, since `Color` is used as a type argument in [encodeSerializableElement][CompositeEncoder.encodeSerializableElement] function, `value.color` will be boxed
109to `Color` wrapper before passing it to the function, preventing the value class optimization. To avoid this, we can use
110special [encodeInlineElement][CompositeEncoder.encodeInlineElement] function instead. It uses [serial descriptor][SerialDescriptor] of `Color` ([retrieved][SerialDescriptor.getElementDescriptor] from serial descriptor of `NamedColor`) instead of [KSerializer],
111does not have type parameters and does not accept any values. Instead, it returns [Encoder]. Using it, we can encode
112unboxed value:
113
114```kotlin
115override fun serialize(encoder: Encoder, value: NamedColor) {
116  encoder.encodeStructure(descriptor) {
117    encodeInlineElement(descriptor, 0).encodeInt(value.color)
118    encodeStringElement(descriptor, 1, value.name)
119  }
120}
121```
122
123The same principle goes also with [CompositeDecoder]: it has [decodeInlineElement][CompositeDecoder.decodeInlineElement] function that returns [Decoder].
124
125If your class should be represented as a primitive (as shown in [Primitive serializer](serializers.md#primitive-serializer) section),
126and you cannot use [encodeStructure][Encoder.encodeStructure] function, there is a complementary function in [Encoder] called [encodeInline][Encoder.encodeInline].
127We will use it to show an example how one can represent a class as an unsigned integer.
128
129Let's start with a UID class:
130
131```kotlin
132@Serializable(UIDSerializer::class)
133class UID(val uid: Int)
134```
135
136`uid` type is `Int`, but suppose we want it to be an unsigned integer in JSON. We can start writing the
137following custom serializer:
138
139```kotlin
140object UIDSerializer: KSerializer<UID> {
141  override val descriptor = UInt.serializer().descriptor
142}
143```
144
145Note that we are using here descriptor from `UInt.serializer()` — it means that the class' representation looks like a
146UInt's one.
147
148Then the `serialize` method:
149
150```kotlin
151override fun serialize(encoder: Encoder, value: UID) {
152  encoder.encodeInline(descriptor).encodeInt(value.uid)
153}
154```
155
156That's where the magic happens — despite we called a regular [encodeInt][Encoder.encodeInt] with a `uid: Int` argument, the output will contain
157an unsigned int because of the special encoder from `encodeInline` function. Since JSON format supports unsigned integers, it
158recognizes theirs descriptors when they're passed into `encodeInline` and handles consecutive calls as for unsigned integers.
159
160The `deserialize` method looks symmetrically:
161
162```kotlin
163override fun deserialize(decoder: Decoder): UID {
164  return UID(decoder.decodeInline(descriptor).decodeInt())
165}
166```
167
168> 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).
169> However, do not use anything in custom serializers for value classes besides `encodeInline`. As we discussed, calls to value class serializer may be
170> optimized and replaced with a `encodeInlineElement` calls.
171> `encodeInline` and `encodeInlineElement` calls with the same descriptor are considered equivalent and can be replaced with each other — formats should return the same `Encoder`.
172> If you embed custom logic in custom value class serializer, you may get different results depending on whether this serializer was called at all
173> (and this, in turn, depends on whether value class was boxed or not).
174
175---
176
177<!--- MODULE /kotlinx-serialization-core -->
178<!--- INDEX kotlinx-serialization-core/kotlinx.serialization -->
179
180[KSerializer]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-k-serializer/index.html
181
182<!--- INDEX kotlinx-serialization-core/kotlinx.serialization.encoding -->
183
184[CompositeEncoder.encodeSerializableElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-composite-encoder/encode-serializable-element.html
185[CompositeEncoder.encodeInlineElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-composite-encoder/encode-inline-element.html
186[Encoder]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/index.html
187[CompositeDecoder]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-composite-decoder/index.html
188[CompositeDecoder.decodeInlineElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-composite-decoder/decode-inline-element.html
189[Decoder]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-decoder/index.html
190[Encoder.encodeStructure]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/encode-structure.html
191[Encoder.encodeInline]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/encode-inline.html
192[Encoder.encodeInt]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.encoding/-encoder/encode-int.html
193
194<!--- INDEX kotlinx-serialization-core/kotlinx.serialization.descriptors -->
195
196[SerialDescriptor]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.descriptors/-serial-descriptor/index.html
197[SerialDescriptor.getElementDescriptor]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization.descriptors/-serial-descriptor/get-element-descriptor.html
198
199<!--- MODULE /kotlinx-serialization-json -->
200<!--- INDEX kotlinx-serialization-json/kotlinx.serialization.json -->
201
202[Json]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/index.html
203
204<!--- END -->
205