Skip to content

Commit 13a487c

Browse files
authored
Add kotlin.time.Instant serializers (#2945)
Add kotlin.time.Instant serializers kotlinx.datetime.Instant entered the stdlib as kotlin.time.Instant, and so kotlinx.serialization takes over its serializers. (toString() one and ComponentSerializer). See Kotlin/KEEP#387 * Move the tests for the Instant serializers Original version: https://github.com/Kotlin/kotlinx-datetime/blob/72681c2acaf9addf5effdef8ecd0975f6f7d10a7/serialization/common/test/InstantSerializationTest.kt * Improve the documentation
1 parent 31e92f2 commit 13a487c

File tree

12 files changed

+246
-6
lines changed

12 files changed

+246
-6
lines changed

core/api/kotlinx-serialization-core.api

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,19 @@ public final class kotlinx/serialization/builtins/BuiltinSerializersKt {
202202
public static final fun serializer (Lkotlin/jvm/internal/ShortCompanionObject;)Lkotlinx/serialization/KSerializer;
203203
public static final fun serializer (Lkotlin/jvm/internal/StringCompanionObject;)Lkotlinx/serialization/KSerializer;
204204
public static final fun serializer (Lkotlin/time/Duration$Companion;)Lkotlinx/serialization/KSerializer;
205+
public static final fun serializer (Lkotlin/time/Instant$Companion;)Lkotlinx/serialization/KSerializer;
205206
public static final fun serializer (Lkotlin/uuid/Uuid$Companion;)Lkotlinx/serialization/KSerializer;
206207
}
207208

209+
public final class kotlinx/serialization/builtins/InstantComponentSerializer : kotlinx/serialization/KSerializer {
210+
public static final field INSTANCE Lkotlinx/serialization/builtins/InstantComponentSerializer;
211+
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
212+
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lkotlin/time/Instant;
213+
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
214+
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
215+
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lkotlin/time/Instant;)V
216+
}
217+
208218
public final class kotlinx/serialization/builtins/LongAsStringSerializer : kotlinx/serialization/KSerializer {
209219
public static final field INSTANCE Lkotlinx/serialization/builtins/LongAsStringSerializer;
210220
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Long;
@@ -795,6 +805,15 @@ public final class kotlinx/serialization/internal/InlineClassDescriptorKt {
795805
public static final fun InlinePrimitiveDescriptor (Ljava/lang/String;Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/descriptors/SerialDescriptor;
796806
}
797807

808+
public final class kotlinx/serialization/internal/InstantSerializer : kotlinx/serialization/KSerializer {
809+
public static final field INSTANCE Lkotlinx/serialization/internal/InstantSerializer;
810+
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
811+
public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lkotlin/time/Instant;
812+
public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
813+
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
814+
public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lkotlin/time/Instant;)V
815+
}
816+
798817
public final class kotlinx/serialization/internal/IntArrayBuilder : kotlinx/serialization/internal/PrimitiveArrayBuilder {
799818
public synthetic fun build$kotlinx_serialization_core ()Ljava/lang/Object;
800819
}

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,14 @@ sealed class kotlinx.serialization.modules/SerializersModule { // kotlinx.serial
890890
final fun <#A1: kotlin/Any> getContextual(kotlin.reflect/KClass<#A1>): kotlinx.serialization/KSerializer<#A1>? // kotlinx.serialization.modules/SerializersModule.getContextual|getContextual(kotlin.reflect.KClass<0:0>){0§<kotlin.Any>}[0]
891891
}
892892

893+
final object kotlinx.serialization.builtins/InstantComponentSerializer : kotlinx.serialization/KSerializer<kotlin.time/Instant> { // kotlinx.serialization.builtins/InstantComponentSerializer|null[0]
894+
final val descriptor // kotlinx.serialization.builtins/InstantComponentSerializer.descriptor|{}descriptor[0]
895+
final fun <get-descriptor>(): kotlinx.serialization.descriptors/SerialDescriptor // kotlinx.serialization.builtins/InstantComponentSerializer.descriptor.<get-descriptor>|<get-descriptor>(){}[0]
896+
897+
final fun deserialize(kotlinx.serialization.encoding/Decoder): kotlin.time/Instant // kotlinx.serialization.builtins/InstantComponentSerializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0]
898+
final fun serialize(kotlinx.serialization.encoding/Encoder, kotlin.time/Instant) // kotlinx.serialization.builtins/InstantComponentSerializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;kotlin.time.Instant){}[0]
899+
}
900+
893901
final object kotlinx.serialization.builtins/LongAsStringSerializer : kotlinx.serialization/KSerializer<kotlin/Long> { // kotlinx.serialization.builtins/LongAsStringSerializer|null[0]
894902
final val descriptor // kotlinx.serialization.builtins/LongAsStringSerializer.descriptor|{}descriptor[0]
895903
final fun <get-descriptor>(): kotlinx.serialization.descriptors/SerialDescriptor // kotlinx.serialization.builtins/LongAsStringSerializer.descriptor.<get-descriptor>|<get-descriptor>(){}[0]
@@ -956,6 +964,14 @@ final object kotlinx.serialization.internal/FloatSerializer : kotlinx.serializat
956964
final fun serialize(kotlinx.serialization.encoding/Encoder, kotlin/Float) // kotlinx.serialization.internal/FloatSerializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;kotlin.Float){}[0]
957965
}
958966

967+
final object kotlinx.serialization.internal/InstantSerializer : kotlinx.serialization/KSerializer<kotlin.time/Instant> { // kotlinx.serialization.internal/InstantSerializer|null[0]
968+
final val descriptor // kotlinx.serialization.internal/InstantSerializer.descriptor|{}descriptor[0]
969+
final fun <get-descriptor>(): kotlinx.serialization.descriptors/SerialDescriptor // kotlinx.serialization.internal/InstantSerializer.descriptor.<get-descriptor>|<get-descriptor>(){}[0]
970+
971+
final fun deserialize(kotlinx.serialization.encoding/Decoder): kotlin.time/Instant // kotlinx.serialization.internal/InstantSerializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0]
972+
final fun serialize(kotlinx.serialization.encoding/Encoder, kotlin.time/Instant) // kotlinx.serialization.internal/InstantSerializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;kotlin.time.Instant){}[0]
973+
}
974+
959975
final object kotlinx.serialization.internal/IntArraySerializer : kotlinx.serialization.internal/PrimitiveArraySerializer<kotlin/Int, kotlin/IntArray, kotlinx.serialization.internal/IntArrayBuilder>, kotlinx.serialization/KSerializer<kotlin/IntArray> // kotlinx.serialization.internal/IntArraySerializer|null[0]
960976

961977
final object kotlinx.serialization.internal/IntSerializer : kotlinx.serialization/KSerializer<kotlin/Int> { // kotlinx.serialization.internal/IntSerializer|null[0]
@@ -1074,6 +1090,7 @@ final val kotlinx.serialization.modules/EmptySerializersModule // kotlinx.serial
10741090
final fun <get-EmptySerializersModule>(): kotlinx.serialization.modules/SerializersModule // kotlinx.serialization.modules/EmptySerializersModule.<get-EmptySerializersModule>|<get-EmptySerializersModule>(){}[0]
10751091

10761092
final fun (kotlin.time/Duration.Companion).kotlinx.serialization.builtins/serializer(): kotlinx.serialization/KSerializer<kotlin.time/Duration> // kotlinx.serialization.builtins/serializer|[email protected](){}[0]
1093+
final fun (kotlin.time/Instant.Companion).kotlinx.serialization.builtins/serializer(): kotlinx.serialization/KSerializer<kotlin.time/Instant> // kotlinx.serialization.builtins/serializer|[email protected](){}[0]
10771094
final fun (kotlin.uuid/Uuid.Companion).kotlinx.serialization.builtins/serializer(): kotlinx.serialization/KSerializer<kotlin.uuid/Uuid> // kotlinx.serialization.builtins/serializer|[email protected](){}[0]
10781095
final fun (kotlin/Boolean.Companion).kotlinx.serialization.builtins/serializer(): kotlinx.serialization/KSerializer<kotlin/Boolean> // kotlinx.serialization.builtins/serializer|[email protected](){}[0]
10791096
final fun (kotlin/Byte.Companion).kotlinx.serialization.builtins/serializer(): kotlinx.serialization/KSerializer<kotlin/Byte> // kotlinx.serialization.builtins/serializer|[email protected](){}[0]

core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import kotlinx.serialization.internal.*
1010
import kotlin.reflect.*
1111
import kotlinx.serialization.descriptors.*
1212
import kotlin.time.Duration
13+
import kotlin.time.ExperimentalTime
14+
import kotlin.time.Instant
1315
import kotlin.uuid.*
1416

1517
/**
@@ -245,12 +247,30 @@ public fun UShort.Companion.serializer(): KSerializer<UShort> = UShortSerializer
245247

246248
/**
247249
* Returns serializer for [Duration].
248-
* It is serialized as a string that represents a duration in the ISO-8601-2 format.
250+
* It is serialized as a string that represents a duration in the format used by [Duration.toIsoString],
251+
* that is, the ISO-8601-2 format.
249252
*
250-
* The result of serialization is similar to calling [Duration.toIsoString], for deserialization is [Duration.parseIsoString].
253+
* For deserialization, [Duration.parseIsoString] is used.
254+
*
255+
* @see Duration.toIsoString
256+
* @see Duration.parseIsoString
251257
*/
252258
public fun Duration.Companion.serializer(): KSerializer<Duration> = DurationSerializer
253259

260+
/**
261+
* Returns serializer for [Instant].
262+
* It is serialized as a string that represents an instant in the format used by [Instant.toString]
263+
* and described in ISO-8601-1:2019, 5.4.2.1b).
264+
*
265+
* Deserialization is case-insensitive.
266+
* More details can be found in the documentation of [Instant.toString] and [Instant.parse] functions.
267+
*
268+
* @see Instant.toString
269+
* @see Instant.parse
270+
*/
271+
@ExperimentalTime
272+
public fun Instant.Companion.serializer(): KSerializer<Instant> = InstantSerializer
273+
254274
/**
255275
* Returns serializer for [Uuid].
256276
* Serializer operates with a standard UUID string representation, also known as "hex-and-dash" format —
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2025-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.builtins
6+
7+
import kotlinx.serialization.*
8+
import kotlinx.serialization.descriptors.*
9+
import kotlinx.serialization.encoding.*
10+
import kotlin.time.ExperimentalTime
11+
import kotlin.time.Instant
12+
13+
/**
14+
* Serializer that encodes and decodes [Instant] as its second and nanosecond components of the Unix time.
15+
*
16+
* JSON example: `{"epochSeconds":1607505416,"nanosecondsOfSecond":124000}`.
17+
*/
18+
@ExperimentalTime
19+
public object InstantComponentSerializer : KSerializer<Instant> {
20+
21+
override val descriptor: SerialDescriptor =
22+
buildClassSerialDescriptor("kotlinx.serialization.InstantComponentSerializer") {
23+
element<Long>("epochSeconds")
24+
element<Long>("nanosecondsOfSecond", isOptional = true)
25+
}
26+
27+
@OptIn(ExperimentalSerializationApi::class)
28+
override fun deserialize(decoder: Decoder): Instant =
29+
decoder.decodeStructure(descriptor) {
30+
var epochSecondsNotSeen = true
31+
var epochSeconds: Long = 0
32+
var nanosecondsOfSecond = 0
33+
while (true) {
34+
when (val index = decodeElementIndex(descriptor)) {
35+
0 -> {
36+
epochSecondsNotSeen = false
37+
epochSeconds = decodeLongElement(descriptor, 0)
38+
}
39+
1 -> nanosecondsOfSecond = decodeIntElement(descriptor, 1)
40+
CompositeDecoder.DECODE_DONE -> break
41+
else -> throw SerializationException("Unexpected index: $index")
42+
}
43+
}
44+
if (epochSecondsNotSeen) throw MissingFieldException(
45+
missingField = "epochSeconds",
46+
serialName = descriptor.serialName
47+
)
48+
Instant.fromEpochSeconds(epochSeconds, nanosecondsOfSecond)
49+
}
50+
51+
@OptIn(ExperimentalSerializationApi::class)
52+
override fun serialize(encoder: Encoder, value: Instant) {
53+
encoder.encodeStructure(descriptor) {
54+
encodeLongElement(descriptor, 0, value.epochSeconds)
55+
if (value.nanosecondsOfSecond != 0 || shouldEncodeElementDefault(descriptor, 1)) {
56+
encodeIntElement(descriptor, 1, value.nanosecondsOfSecond)
57+
}
58+
}
59+
}
60+
61+
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import kotlinx.serialization.descriptors.SerialDescriptor
1010
import kotlinx.serialization.encoding.Decoder
1111
import kotlinx.serialization.encoding.Encoder
1212
import kotlin.time.Duration
13+
import kotlin.time.ExperimentalTime
14+
import kotlin.time.Instant
1315
import kotlin.uuid.*
1416

1517

@@ -39,6 +41,20 @@ internal object NothingSerializer : KSerializer<Nothing> {
3941
}
4042
}
4143

44+
@PublishedApi
45+
@ExperimentalTime
46+
internal object InstantSerializer : KSerializer<Instant> {
47+
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlin.time.Instant", PrimitiveKind.STRING)
48+
49+
override fun serialize(encoder: Encoder, value: Instant) {
50+
encoder.encodeString(value.toString())
51+
}
52+
53+
override fun deserialize(decoder: Decoder): Instant {
54+
return Instant.parse(decoder.decodeString())
55+
}
56+
}
57+
4258
@PublishedApi
4359
@ExperimentalUuidApi
4460
internal object UuidSerializer: KSerializer<Uuid> {

core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import kotlinx.serialization.descriptors.*
1010
import kotlinx.serialization.encoding.*
1111
import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME
1212
import kotlinx.serialization.modules.*
13-
import kotlinx.serialization.test.*
1413
import kotlin.test.*
1514
import kotlin.time.Duration
1615

core/jsMain/src/kotlinx/serialization/internal/Platform.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ private val KClass<*>.isInterface: Boolean
8181
return js.asDynamic().`$metadata$`?.kind == "interface"
8282
}
8383

84-
@OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class, ExperimentalSerializationApi::class)
84+
@OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class, ExperimentalSerializationApi::class,
85+
ExperimentalTime::class)
8586
internal actual fun initBuiltins(): Map<KClass<*>, KSerializer<*>> = mapOf(
8687
String::class to String.serializer(),
8788
Char::class to Char.serializer(),
@@ -111,5 +112,6 @@ internal actual fun initBuiltins(): Map<KClass<*>, KSerializer<*>> = mapOf(
111112
Unit::class to Unit.serializer(),
112113
Nothing::class to NothingSerializer(),
113114
Duration::class to Duration.serializer(),
115+
Instant::class to Instant.serializer(),
114116
Uuid::class to Uuid.serializer()
115117
)

core/jvmMain/src/kotlinx/serialization/internal/Platform.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ internal actual fun initBuiltins(): Map<KClass<*>, KSerializer<*>> = buildMap {
201201
}
202202
@OptIn(ExperimentalUuidApi::class)
203203
loadSafe { put(Uuid::class, Uuid.serializer()) }
204+
205+
@OptIn(ExperimentalTime::class)
206+
loadSafe { put(Instant::class, Instant.serializer()) }
204207
}
205208

206209
// Reference classes in [block] ignoring any exceptions related to class loading

core/nativeMain/src/kotlinx/serialization/internal/Platform.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ private fun <T> arrayOfAnyNulls(size: Int): Array<T> = arrayOfNulls<Any>(size) a
7575

7676
internal actual fun isReferenceArray(rootClass: KClass<Any>): Boolean = rootClass == Array::class
7777

78-
@OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class, ExperimentalSerializationApi::class)
78+
@OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class, ExperimentalSerializationApi::class,
79+
ExperimentalTime::class)
7980
internal actual fun initBuiltins(): Map<KClass<*>, KSerializer<*>> = mapOf(
8081
String::class to String.serializer(),
8182
Char::class to Char.serializer(),
@@ -105,5 +106,6 @@ internal actual fun initBuiltins(): Map<KClass<*>, KSerializer<*>> = mapOf(
105106
Unit::class to Unit.serializer(),
106107
Nothing::class to NothingSerializer(),
107108
Duration::class to Duration.serializer(),
109+
Instant::class to Instant.serializer(),
108110
Uuid::class to Uuid.serializer()
109111
)

core/wasmMain/src/kotlinx/serialization/internal/Platform.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ internal actual fun <T : Any, E : T?> ArrayList<E>.toNativeArrayImpl(eClass: KCl
6565

6666
internal actual fun isReferenceArray(rootClass: KClass<Any>): Boolean = rootClass == Array::class
6767

68-
@OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class, ExperimentalSerializationApi::class)
68+
@OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class, ExperimentalSerializationApi::class,
69+
ExperimentalTime::class)
6970
internal actual fun initBuiltins(): Map<KClass<*>, KSerializer<*>> = mapOf(
7071
String::class to String.serializer(),
7172
Char::class to Char.serializer(),
@@ -95,5 +96,6 @@ internal actual fun initBuiltins(): Map<KClass<*>, KSerializer<*>> = mapOf(
9596
Unit::class to Unit.serializer(),
9697
Nothing::class to NothingSerializer(),
9798
Duration::class to Duration.serializer(),
99+
Instant::class to Instant.serializer(),
98100
Uuid::class to Uuid.serializer()
99101
)

0 commit comments

Comments
 (0)