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