Skip to content

Commit 88b69d5

Browse files
committed
Add kotlin.time.Instant serializers
Can be merged after moving to Kotlin 2.1.20, which introduces kotlin.time.Instant. kotlinx.datetime.Instant entered the stdlib as kotlin.time.Instant, and so kotlinx.serialization takes over its serializers. See Kotlin/KEEP#387
1 parent 831468e commit 88b69d5

File tree

11 files changed

+154
-5
lines changed

11 files changed

+154
-5
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: 15 additions & 0 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
/**
@@ -251,6 +253,19 @@ public fun UShort.Companion.serializer(): KSerializer<UShort> = UShortSerializer
251253
*/
252254
public fun Duration.Companion.serializer(): KSerializer<Duration> = DurationSerializer
253255

256+
/**
257+
* Returns serializer for [Instant].
258+
* It is serialized as a string that represents an instant in the format described in ISO-8601-1:2019, 5.4.2.1b).
259+
*
260+
* Deserialization is case-insensitive.
261+
* More details can be found in the documentation of [Instant.toString] and [Instant.parse] functions.
262+
*
263+
* @see Instant.toString
264+
* @see Instant.parse
265+
*/
266+
@ExperimentalTime
267+
public fun Instant.Companion.serializer(): KSerializer<Instant> = InstantSerializer
268+
254269
/**
255270
* Returns serializer for [Uuid].
256271
* Serializer operates with a standard UUID string representation, also known as "hex-and-dash" format —
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
@ExperimentalTime
14+
public object InstantComponentSerializer : KSerializer<Instant> {
15+
16+
override val descriptor: SerialDescriptor =
17+
buildClassSerialDescriptor("kotlinx.serialization.InstantComponentSerializer") {
18+
element<Long>("epochSeconds")
19+
element<Long>("nanosecondsOfSecond", isOptional = true)
20+
}
21+
22+
@OptIn(ExperimentalSerializationApi::class)
23+
override fun deserialize(decoder: Decoder): Instant =
24+
decoder.decodeStructure(descriptor) {
25+
var epochSeconds: Long? = null
26+
var nanosecondsOfSecond = 0
27+
loop@ while (true) {
28+
when (val index = decodeElementIndex(descriptor)) {
29+
0 -> epochSeconds = decodeLongElement(descriptor, 0)
30+
1 -> nanosecondsOfSecond = decodeIntElement(descriptor, 1)
31+
CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262
32+
else -> throw SerializationException("Unexpected index: $index")
33+
}
34+
}
35+
if (epochSeconds == null) throw MissingFieldException(
36+
missingField = "epochSeconds",
37+
serialName = descriptor.serialName
38+
)
39+
Instant.fromEpochSeconds(epochSeconds, nanosecondsOfSecond)
40+
}
41+
42+
override fun serialize(encoder: Encoder, value: Instant) {
43+
encoder.encodeStructure(descriptor) {
44+
encodeLongElement(descriptor, 0, value.epochSeconds)
45+
if (value.nanosecondsOfSecond != 0) {
46+
encodeIntElement(descriptor, 1, value.nanosecondsOfSecond)
47+
}
48+
}
49+
}
50+
51+
}

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: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ 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
15+
import kotlin.time.Instant
16+
import kotlin.time.ExperimentalTime
1617

1718
/*
1819
* Test ensures that type that aggregate all basic (primitive/collection/maps/arrays)
@@ -193,6 +194,27 @@ class BasicTypesSerializationTest {
193194
assertEquals(Duration.parseIsoString(durationString), other)
194195
}
195196

197+
@OptIn(ExperimentalTime::class)
198+
@Test
199+
fun testEncodeInstant() {
200+
val sb = StringBuilder()
201+
val out = KeyValueOutput(sb)
202+
203+
val instant = Instant.parse("2020-12-09T09:16:56.000124Z")
204+
out.encodeSerializableValue(Instant.serializer(), instant)
205+
206+
assertEquals("\"${instant}\"", sb.toString())
207+
}
208+
209+
@OptIn(ExperimentalTime::class)
210+
@Test
211+
fun testDecodeInstant() {
212+
val instantString = "2020-12-09T09:16:56.000124Z"
213+
val inp = KeyValueInput(Parser(StringReader("\"$instantString\"")))
214+
val other = inp.decodeSerializableValue(Instant.serializer())
215+
assertEquals(Instant.parse(instantString), other)
216+
}
217+
196218
@Test
197219
fun testNothingSerialization() {
198220
// impossible to deserialize Nothing

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
)

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[versions]
2-
kotlin = "2.1.0"
2+
kotlin = "2.1.20-RC"
33
kover = "0.8.2"
44
dokka = "2.0.0-Beta"
55
knit = "0.5.0"

0 commit comments

Comments
 (0)