From 288f73c3fd5e42d6d1db5bc68d1c0174c3346836 Mon Sep 17 00:00:00 2001 From: Oleg Yukhnevich Date: Thu, 10 Apr 2025 16:54:00 +0300 Subject: [PATCH 1/9] Update to Dokka 2.0.0 (#2927) --- gradle.properties | 2 -- gradle/libs.versions.toml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 89fe62072..deb1585fa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,5 +23,3 @@ org.gradle.kotlin.dsl.allWarningsAsErrors=true kotlin.native.distribution.type=prebuilt org.gradle.jvmargs="-XX:+HeapDumpOnOutOfMemoryError" - -org.jetbrains.dokka.experimental.tryK2=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 22b2552b8..132a67b61 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] kotlin = "2.1.20" kover = "0.8.2" -dokka = "2.0.0-Beta" +dokka = "2.0.0" knit = "0.5.0" bcv = "0.16.2" animalsniffer = "1.7.1" From 4a0530d29bc09d1978a2fd80447ea4d1408004c9 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 4 Mar 2025 16:39:14 +0100 Subject: [PATCH 2/9] 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 https://github.com/Kotlin/KEEP/pull/387 --- core/api/kotlinx-serialization-core.api | 19 +++++++ core/api/kotlinx-serialization-core.klib.api | 17 +++++++ .../builtins/BuiltinSerializers.kt | 15 ++++++ .../builtins/InstantComponentSerializer.kt | 51 +++++++++++++++++++ .../internal/BuiltInSerializers.kt | 16 ++++++ .../BasicTypesSerializationTest.kt | 24 ++++++++- .../serialization/internal/Platform.kt | 4 +- .../serialization/internal/Platform.kt | 3 ++ .../serialization/internal/Platform.kt | 4 +- .../serialization/internal/Platform.kt | 4 +- 10 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt diff --git a/core/api/kotlinx-serialization-core.api b/core/api/kotlinx-serialization-core.api index c8d0d35d7..6a2c1a20d 100644 --- a/core/api/kotlinx-serialization-core.api +++ b/core/api/kotlinx-serialization-core.api @@ -202,9 +202,19 @@ public final class kotlinx/serialization/builtins/BuiltinSerializersKt { public static final fun serializer (Lkotlin/jvm/internal/ShortCompanionObject;)Lkotlinx/serialization/KSerializer; public static final fun serializer (Lkotlin/jvm/internal/StringCompanionObject;)Lkotlinx/serialization/KSerializer; public static final fun serializer (Lkotlin/time/Duration$Companion;)Lkotlinx/serialization/KSerializer; + public static final fun serializer (Lkotlin/time/Instant$Companion;)Lkotlinx/serialization/KSerializer; public static final fun serializer (Lkotlin/uuid/Uuid$Companion;)Lkotlinx/serialization/KSerializer; } +public final class kotlinx/serialization/builtins/InstantComponentSerializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Lkotlinx/serialization/builtins/InstantComponentSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lkotlin/time/Instant; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lkotlin/time/Instant;)V +} + public final class kotlinx/serialization/builtins/LongAsStringSerializer : kotlinx/serialization/KSerializer { public static final field INSTANCE Lkotlinx/serialization/builtins/LongAsStringSerializer; public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Long; @@ -795,6 +805,15 @@ public final class kotlinx/serialization/internal/InlineClassDescriptorKt { public static final fun InlinePrimitiveDescriptor (Ljava/lang/String;Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/descriptors/SerialDescriptor; } +public final class kotlinx/serialization/internal/InstantSerializer : kotlinx/serialization/KSerializer { + public static final field INSTANCE Lkotlinx/serialization/internal/InstantSerializer; + public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; + public fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lkotlin/time/Instant; + public fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; + public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V + public fun serialize (Lkotlinx/serialization/encoding/Encoder;Lkotlin/time/Instant;)V +} + public final class kotlinx/serialization/internal/IntArrayBuilder : kotlinx/serialization/internal/PrimitiveArrayBuilder { public synthetic fun build$kotlinx_serialization_core ()Ljava/lang/Object; } diff --git a/core/api/kotlinx-serialization-core.klib.api b/core/api/kotlinx-serialization-core.klib.api index 041c115e2..5aefad0f2 100644 --- a/core/api/kotlinx-serialization-core.klib.api +++ b/core/api/kotlinx-serialization-core.klib.api @@ -890,6 +890,14 @@ sealed class kotlinx.serialization.modules/SerializersModule { // kotlinx.serial 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§}[0] } +final object kotlinx.serialization.builtins/InstantComponentSerializer : kotlinx.serialization/KSerializer { // kotlinx.serialization.builtins/InstantComponentSerializer|null[0] + final val descriptor // kotlinx.serialization.builtins/InstantComponentSerializer.descriptor|{}descriptor[0] + final fun (): kotlinx.serialization.descriptors/SerialDescriptor // kotlinx.serialization.builtins/InstantComponentSerializer.descriptor.|(){}[0] + + final fun deserialize(kotlinx.serialization.encoding/Decoder): kotlin.time/Instant // kotlinx.serialization.builtins/InstantComponentSerializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0] + final fun serialize(kotlinx.serialization.encoding/Encoder, kotlin.time/Instant) // kotlinx.serialization.builtins/InstantComponentSerializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;kotlin.time.Instant){}[0] +} + final object kotlinx.serialization.builtins/LongAsStringSerializer : kotlinx.serialization/KSerializer { // kotlinx.serialization.builtins/LongAsStringSerializer|null[0] final val descriptor // kotlinx.serialization.builtins/LongAsStringSerializer.descriptor|{}descriptor[0] final fun (): kotlinx.serialization.descriptors/SerialDescriptor // kotlinx.serialization.builtins/LongAsStringSerializer.descriptor.|(){}[0] @@ -956,6 +964,14 @@ final object kotlinx.serialization.internal/FloatSerializer : kotlinx.serializat final fun serialize(kotlinx.serialization.encoding/Encoder, kotlin/Float) // kotlinx.serialization.internal/FloatSerializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;kotlin.Float){}[0] } +final object kotlinx.serialization.internal/InstantSerializer : kotlinx.serialization/KSerializer { // kotlinx.serialization.internal/InstantSerializer|null[0] + final val descriptor // kotlinx.serialization.internal/InstantSerializer.descriptor|{}descriptor[0] + final fun (): kotlinx.serialization.descriptors/SerialDescriptor // kotlinx.serialization.internal/InstantSerializer.descriptor.|(){}[0] + + final fun deserialize(kotlinx.serialization.encoding/Decoder): kotlin.time/Instant // kotlinx.serialization.internal/InstantSerializer.deserialize|deserialize(kotlinx.serialization.encoding.Decoder){}[0] + final fun serialize(kotlinx.serialization.encoding/Encoder, kotlin.time/Instant) // kotlinx.serialization.internal/InstantSerializer.serialize|serialize(kotlinx.serialization.encoding.Encoder;kotlin.time.Instant){}[0] +} + final object kotlinx.serialization.internal/IntArraySerializer : kotlinx.serialization.internal/PrimitiveArraySerializer, kotlinx.serialization/KSerializer // kotlinx.serialization.internal/IntArraySerializer|null[0] final object kotlinx.serialization.internal/IntSerializer : kotlinx.serialization/KSerializer { // kotlinx.serialization.internal/IntSerializer|null[0] @@ -1074,6 +1090,7 @@ final val kotlinx.serialization.modules/EmptySerializersModule // kotlinx.serial final fun (): kotlinx.serialization.modules/SerializersModule // kotlinx.serialization.modules/EmptySerializersModule.|(){}[0] final fun (kotlin.time/Duration.Companion).kotlinx.serialization.builtins/serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.builtins/serializer|serializer@kotlin.time.Duration.Companion(){}[0] +final fun (kotlin.time/Instant.Companion).kotlinx.serialization.builtins/serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.builtins/serializer|serializer@kotlin.time.Instant.Companion(){}[0] final fun (kotlin.uuid/Uuid.Companion).kotlinx.serialization.builtins/serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.builtins/serializer|serializer@kotlin.uuid.Uuid.Companion(){}[0] final fun (kotlin/Boolean.Companion).kotlinx.serialization.builtins/serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.builtins/serializer|serializer@kotlin.Boolean.Companion(){}[0] final fun (kotlin/Byte.Companion).kotlinx.serialization.builtins/serializer(): kotlinx.serialization/KSerializer // kotlinx.serialization.builtins/serializer|serializer@kotlin.Byte.Companion(){}[0] diff --git a/core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt b/core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt index c481e3adf..a7099114a 100644 --- a/core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt +++ b/core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt @@ -10,6 +10,8 @@ import kotlinx.serialization.internal.* import kotlin.reflect.* import kotlinx.serialization.descriptors.* import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlin.uuid.* /** @@ -251,6 +253,19 @@ public fun UShort.Companion.serializer(): KSerializer = UShortSerializer */ public fun Duration.Companion.serializer(): KSerializer = DurationSerializer +/** + * Returns serializer for [Instant]. + * It is serialized as a string that represents an instant in the format described in ISO-8601-1:2019, 5.4.2.1b). + * + * Deserialization is case-insensitive. + * More details can be found in the documentation of [Instant.toString] and [Instant.parse] functions. + * + * @see Instant.toString + * @see Instant.parse + */ +@ExperimentalTime +public fun Instant.Companion.serializer(): KSerializer = InstantSerializer + /** * Returns serializer for [Uuid]. * Serializer operates with a standard UUID string representation, also known as "hex-and-dash" format — diff --git a/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt b/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt new file mode 100644 index 000000000..a2f3e58d9 --- /dev/null +++ b/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2025-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.builtins + +import kotlinx.serialization.* +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlin.time.ExperimentalTime +import kotlin.time.Instant + +@ExperimentalTime +public object InstantComponentSerializer : KSerializer { + + override val descriptor: SerialDescriptor = + buildClassSerialDescriptor("kotlinx.serialization.InstantComponentSerializer") { + element("epochSeconds") + element("nanosecondsOfSecond", isOptional = true) + } + + @OptIn(ExperimentalSerializationApi::class) + override fun deserialize(decoder: Decoder): Instant = + decoder.decodeStructure(descriptor) { + var epochSeconds: Long? = null + var nanosecondsOfSecond = 0 + loop@ while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> epochSeconds = decodeLongElement(descriptor, 0) + 1 -> nanosecondsOfSecond = decodeIntElement(descriptor, 1) + CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262 + else -> throw SerializationException("Unexpected index: $index") + } + } + if (epochSeconds == null) throw MissingFieldException( + missingField = "epochSeconds", + serialName = descriptor.serialName + ) + Instant.fromEpochSeconds(epochSeconds, nanosecondsOfSecond) + } + + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeStructure(descriptor) { + encodeLongElement(descriptor, 0, value.epochSeconds) + if (value.nanosecondsOfSecond != 0) { + encodeIntElement(descriptor, 1, value.nanosecondsOfSecond) + } + } + } + +} diff --git a/core/commonMain/src/kotlinx/serialization/internal/BuiltInSerializers.kt b/core/commonMain/src/kotlinx/serialization/internal/BuiltInSerializers.kt index fbc5dc147..8db48f72e 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/BuiltInSerializers.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/BuiltInSerializers.kt @@ -10,6 +10,8 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlin.time.Duration +import kotlin.time.ExperimentalTime +import kotlin.time.Instant import kotlin.uuid.* @@ -39,6 +41,20 @@ internal object NothingSerializer : KSerializer { } } +@PublishedApi +@ExperimentalTime +internal object InstantSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("kotlin.time.Instant", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): Instant { + return Instant.parse(decoder.decodeString()) + } +} + @PublishedApi @ExperimentalUuidApi internal object UuidSerializer: KSerializer { diff --git a/core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt b/core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt index 859818aa2..f6d04f1b6 100644 --- a/core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt +++ b/core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt @@ -10,9 +10,10 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME import kotlinx.serialization.modules.* -import kotlinx.serialization.test.* import kotlin.test.* import kotlin.time.Duration +import kotlin.time.Instant +import kotlin.time.ExperimentalTime /* * Test ensures that type that aggregate all basic (primitive/collection/maps/arrays) @@ -193,6 +194,27 @@ class BasicTypesSerializationTest { assertEquals(Duration.parseIsoString(durationString), other) } + @OptIn(ExperimentalTime::class) + @Test + fun testEncodeInstant() { + val sb = StringBuilder() + val out = KeyValueOutput(sb) + + val instant = Instant.parse("2020-12-09T09:16:56.000124Z") + out.encodeSerializableValue(Instant.serializer(), instant) + + assertEquals("\"${instant}\"", sb.toString()) + } + + @OptIn(ExperimentalTime::class) + @Test + fun testDecodeInstant() { + val instantString = "2020-12-09T09:16:56.000124Z" + val inp = KeyValueInput(Parser(StringReader("\"$instantString\""))) + val other = inp.decodeSerializableValue(Instant.serializer()) + assertEquals(Instant.parse(instantString), other) + } + @Test fun testNothingSerialization() { // impossible to deserialize Nothing diff --git a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt index e423afca2..72625567f 100644 --- a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt @@ -81,7 +81,8 @@ private val KClass<*>.isInterface: Boolean return js.asDynamic().`$metadata$`?.kind == "interface" } -@OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class, ExperimentalSerializationApi::class) +@OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class, ExperimentalSerializationApi::class, + ExperimentalTime::class) internal actual fun initBuiltins(): Map, KSerializer<*>> = mapOf( String::class to String.serializer(), Char::class to Char.serializer(), @@ -111,5 +112,6 @@ internal actual fun initBuiltins(): Map, KSerializer<*>> = mapOf( Unit::class to Unit.serializer(), Nothing::class to NothingSerializer(), Duration::class to Duration.serializer(), + Instant::class to Instant.serializer(), Uuid::class to Uuid.serializer() ) diff --git a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt index b838a0fe5..65d38fba7 100644 --- a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt @@ -201,6 +201,9 @@ internal actual fun initBuiltins(): Map, KSerializer<*>> = buildMap { } @OptIn(ExperimentalUuidApi::class) loadSafe { put(Uuid::class, Uuid.serializer()) } + + @OptIn(ExperimentalTime::class) + loadSafe { put(Instant::class, Instant.serializer()) } } // Reference classes in [block] ignoring any exceptions related to class loading diff --git a/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt b/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt index c2e9fdd7e..7e2423a76 100644 --- a/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt @@ -75,7 +75,8 @@ private fun arrayOfAnyNulls(size: Int): Array = arrayOfNulls(size) a internal actual fun isReferenceArray(rootClass: KClass): Boolean = rootClass == Array::class -@OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class, ExperimentalSerializationApi::class) +@OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class, ExperimentalSerializationApi::class, + ExperimentalTime::class) internal actual fun initBuiltins(): Map, KSerializer<*>> = mapOf( String::class to String.serializer(), Char::class to Char.serializer(), @@ -105,5 +106,6 @@ internal actual fun initBuiltins(): Map, KSerializer<*>> = mapOf( Unit::class to Unit.serializer(), Nothing::class to NothingSerializer(), Duration::class to Duration.serializer(), + Instant::class to Instant.serializer(), Uuid::class to Uuid.serializer() ) diff --git a/core/wasmMain/src/kotlinx/serialization/internal/Platform.kt b/core/wasmMain/src/kotlinx/serialization/internal/Platform.kt index ecfee8ede..381d6a4d6 100644 --- a/core/wasmMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/wasmMain/src/kotlinx/serialization/internal/Platform.kt @@ -65,7 +65,8 @@ internal actual fun ArrayList.toNativeArrayImpl(eClass: KCl internal actual fun isReferenceArray(rootClass: KClass): Boolean = rootClass == Array::class -@OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class, ExperimentalSerializationApi::class) +@OptIn(ExperimentalUnsignedTypes::class, ExperimentalUuidApi::class, ExperimentalSerializationApi::class, + ExperimentalTime::class) internal actual fun initBuiltins(): Map, KSerializer<*>> = mapOf( String::class to String.serializer(), Char::class to Char.serializer(), @@ -95,5 +96,6 @@ internal actual fun initBuiltins(): Map, KSerializer<*>> = mapOf( Unit::class to Unit.serializer(), Nothing::class to NothingSerializer(), Duration::class to Duration.serializer(), + Instant::class to Instant.serializer(), Uuid::class to Uuid.serializer() ) From dd9909fcba3b957f3bfd1bb41e4cf8ae8097d84a Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 4 Mar 2025 17:00:01 +0100 Subject: [PATCH 3/9] Move the tests for the Instant serializers Original version: https://github.com/Kotlin/kotlinx-datetime/blob/72681c2acaf9addf5effdef8ecd0975f6f7d10a7/serialization/common/test/InstantSerializationTest.kt --- .../serialization/InstantSerializationTest.kt | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt new file mode 100644 index 000000000..74f657fc3 --- /dev/null +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2025-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization + +import kotlinx.serialization.* +import kotlinx.serialization.json.* +import kotlinx.serialization.builtins.* +import kotlin.time.* +import kotlin.test.* + +@OptIn(ExperimentalTime::class) +class InstantSerializationTest { + private fun iso8601Serialization(serializer: KSerializer) { + for ((instant, json) in listOf( + Pair(Instant.fromEpochSeconds(1607505416, 124000), + "\"2020-12-09T09:16:56.000124Z\""), + Pair(Instant.fromEpochSeconds(-1607505416, -124000), + "\"1919-01-23T14:43:03.999876Z\""), + Pair(Instant.fromEpochSeconds(987654321, 123456789), + "\"2001-04-19T04:25:21.123456789Z\""), + Pair(Instant.fromEpochSeconds(987654321, 0), + "\"2001-04-19T04:25:21Z\""), + )) { + assertEquals(json, Json.encodeToString(serializer, instant)) + assertEquals(instant, Json.decodeFromString(serializer, json)) + } + } + + private fun componentSerialization(serializer: KSerializer) { + for ((instant, json) in listOf( + Pair(Instant.fromEpochSeconds(1607505416, 124000), + "{\"epochSeconds\":1607505416,\"nanosecondsOfSecond\":124000}"), + Pair(Instant.fromEpochSeconds(-1607505416, -124000), + "{\"epochSeconds\":-1607505417,\"nanosecondsOfSecond\":999876000}"), + Pair(Instant.fromEpochSeconds(987654321, 123456789), + "{\"epochSeconds\":987654321,\"nanosecondsOfSecond\":123456789}"), + Pair(Instant.fromEpochSeconds(987654321, 0), + "{\"epochSeconds\":987654321}"), + )) { + assertEquals(json, Json.encodeToString(serializer, instant)) + assertEquals(instant, Json.decodeFromString(serializer, json)) + } + // check that having a `"nanosecondsOfSecond": 0` field doesn't break deserialization + assertEquals(Instant.fromEpochSeconds(987654321, 0), + Json.decodeFromString(serializer, + "{\"epochSeconds\":987654321,\"nanosecondsOfSecond\":0}")) + // "epochSeconds" should always be present + assertFailsWith { Json.decodeFromString(serializer, "{}") } + assertFailsWith { Json.decodeFromString(serializer, "{\"nanosecondsOfSecond\":3}") } + } + + @Test + fun testIso8601Serialization() { + iso8601Serialization(Instant.serializer()) + } + + @Test + fun testComponentSerialization() { + componentSerialization(InstantComponentSerializer) + } + + @Test + fun testDefaultSerializers() { + // should be the same as the ISO 8601 + iso8601Serialization(Json.serializersModule.serializer()) + } +} From eea8b48f62abc49dfaccf62712a30a196959e869 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 7 Mar 2025 11:43:16 +0100 Subject: [PATCH 4/9] Improve the documentation --- .../serialization/builtins/BuiltinSerializers.kt | 11 ++++++++--- .../builtins/InstantComponentSerializer.kt | 5 +++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt b/core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt index a7099114a..aedd4fe87 100644 --- a/core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt +++ b/core/commonMain/src/kotlinx/serialization/builtins/BuiltinSerializers.kt @@ -247,15 +247,20 @@ public fun UShort.Companion.serializer(): KSerializer = UShortSerializer /** * Returns serializer for [Duration]. - * It is serialized as a string that represents a duration in the ISO-8601-2 format. + * It is serialized as a string that represents a duration in the format used by [Duration.toIsoString], + * that is, the ISO-8601-2 format. * - * The result of serialization is similar to calling [Duration.toIsoString], for deserialization is [Duration.parseIsoString]. + * For deserialization, [Duration.parseIsoString] is used. + * + * @see Duration.toIsoString + * @see Duration.parseIsoString */ public fun Duration.Companion.serializer(): KSerializer = DurationSerializer /** * Returns serializer for [Instant]. - * It is serialized as a string that represents an instant in the format described in ISO-8601-1:2019, 5.4.2.1b). + * It is serialized as a string that represents an instant in the format used by [Instant.toString] + * and described in ISO-8601-1:2019, 5.4.2.1b). * * Deserialization is case-insensitive. * More details can be found in the documentation of [Instant.toString] and [Instant.parse] functions. diff --git a/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt b/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt index a2f3e58d9..aea043b9d 100644 --- a/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt @@ -10,6 +10,11 @@ import kotlinx.serialization.encoding.* import kotlin.time.ExperimentalTime import kotlin.time.Instant +/** + * Serializer that encodes and decodes [Instant] as its second and nanosecond components of the Unix time. + * + * JSON example: `{"epochSeconds":1607505416,"nanosecondsOfSecond":124000}`. + */ @ExperimentalTime public object InstantComponentSerializer : KSerializer { From 90306110e5d0387db0b52e6f494a2a43cc034aea Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 7 Mar 2025 11:43:55 +0100 Subject: [PATCH 5/9] Remove an old workaround for the JS compiler bug --- .../serialization/builtins/InstantComponentSerializer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt b/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt index aea043b9d..3bbbca433 100644 --- a/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt @@ -29,11 +29,11 @@ public object InstantComponentSerializer : KSerializer { decoder.decodeStructure(descriptor) { var epochSeconds: Long? = null var nanosecondsOfSecond = 0 - loop@ while (true) { + while (true) { when (val index = decodeElementIndex(descriptor)) { 0 -> epochSeconds = decodeLongElement(descriptor, 0) 1 -> nanosecondsOfSecond = decodeIntElement(descriptor, 1) - CompositeDecoder.DECODE_DONE -> break@loop // https://youtrack.jetbrains.com/issue/KT-42262 + CompositeDecoder.DECODE_DONE -> break else -> throw SerializationException("Unexpected index: $index") } } From 22a550950f59587f68757920203c83c22a5caa9f Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 7 Mar 2025 11:44:46 +0100 Subject: [PATCH 6/9] Remove an excessive test --- .../BasicTypesSerializationTest.kt | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt b/core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt index f6d04f1b6..6115f8fd1 100644 --- a/core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt +++ b/core/commonTest/src/kotlinx/serialization/BasicTypesSerializationTest.kt @@ -12,8 +12,6 @@ import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME import kotlinx.serialization.modules.* import kotlin.test.* import kotlin.time.Duration -import kotlin.time.Instant -import kotlin.time.ExperimentalTime /* * Test ensures that type that aggregate all basic (primitive/collection/maps/arrays) @@ -194,27 +192,6 @@ class BasicTypesSerializationTest { assertEquals(Duration.parseIsoString(durationString), other) } - @OptIn(ExperimentalTime::class) - @Test - fun testEncodeInstant() { - val sb = StringBuilder() - val out = KeyValueOutput(sb) - - val instant = Instant.parse("2020-12-09T09:16:56.000124Z") - out.encodeSerializableValue(Instant.serializer(), instant) - - assertEquals("\"${instant}\"", sb.toString()) - } - - @OptIn(ExperimentalTime::class) - @Test - fun testDecodeInstant() { - val instantString = "2020-12-09T09:16:56.000124Z" - val inp = KeyValueInput(Parser(StringReader("\"$instantString\""))) - val other = inp.decodeSerializableValue(Instant.serializer()) - assertEquals(Instant.parse(instantString), other) - } - @Test fun testNothingSerialization() { // impossible to deserialize Nothing From 973a6e8b55d4ab25676b4136387c3c1a3deaa832 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Fri, 7 Mar 2025 12:12:13 +0100 Subject: [PATCH 7/9] Revork the Instant serializers tests --- .../serialization/InstantSerializationTest.kt | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt index 74f657fc3..e2bc5eae8 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt @@ -9,9 +9,10 @@ import kotlinx.serialization.json.* import kotlinx.serialization.builtins.* import kotlin.time.* import kotlin.test.* +import kotlin.reflect.typeOf @OptIn(ExperimentalTime::class) -class InstantSerializationTest { +class InstantSerializationTest: JsonTestBase() { private fun iso8601Serialization(serializer: KSerializer) { for ((instant, json) in listOf( Pair(Instant.fromEpochSeconds(1607505416, 124000), @@ -23,8 +24,7 @@ class InstantSerializationTest { Pair(Instant.fromEpochSeconds(987654321, 0), "\"2001-04-19T04:25:21Z\""), )) { - assertEquals(json, Json.encodeToString(serializer, instant)) - assertEquals(instant, Json.decodeFromString(serializer, json)) + assertJsonFormAndRestored(serializer, instant, json) } } @@ -39,8 +39,7 @@ class InstantSerializationTest { Pair(Instant.fromEpochSeconds(987654321, 0), "{\"epochSeconds\":987654321}"), )) { - assertEquals(json, Json.encodeToString(serializer, instant)) - assertEquals(instant, Json.decodeFromString(serializer, json)) + assertJsonFormAndRestored(serializer, instant, json) } // check that having a `"nanosecondsOfSecond": 0` field doesn't break deserialization assertEquals(Instant.fromEpochSeconds(987654321, 0), @@ -64,6 +63,8 @@ class InstantSerializationTest { @Test fun testDefaultSerializers() { // should be the same as the ISO 8601 - iso8601Serialization(Json.serializersModule.serializer()) + @Suppress("UNCHECKED_CAST") + iso8601Serialization(serializer(typeOf()) as KSerializer) + // iso8601Serialization(serializer()) TODO: uncomment when the compiler adds KT-75759 } } From 75879806d96ba597a042c716de0726493efe3cfa Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Tue, 15 Apr 2025 13:19:17 +0200 Subject: [PATCH 8/9] Address the review --- .../builtins/InstantComponentSerializer.kt | 11 ++++++++--- .../kotlinx/serialization/InstantSerializationTest.kt | 8 ++++++++ .../src/kotlinx/serialization/json/JsonTestBase.kt | 11 +++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt b/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt index 3bbbca433..4340a1181 100644 --- a/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt @@ -27,23 +27,28 @@ public object InstantComponentSerializer : KSerializer { @OptIn(ExperimentalSerializationApi::class) override fun deserialize(decoder: Decoder): Instant = decoder.decodeStructure(descriptor) { - var epochSeconds: Long? = null + var epochSecondsNotSeen = true + var epochSeconds: Long = 0 var nanosecondsOfSecond = 0 while (true) { when (val index = decodeElementIndex(descriptor)) { - 0 -> epochSeconds = decodeLongElement(descriptor, 0) + 0 -> { + epochSecondsNotSeen = false + epochSeconds = decodeLongElement(descriptor, 0) + } 1 -> nanosecondsOfSecond = decodeIntElement(descriptor, 1) CompositeDecoder.DECODE_DONE -> break else -> throw SerializationException("Unexpected index: $index") } } - if (epochSeconds == null) throw MissingFieldException( + if (epochSecondsNotSeen) throw MissingFieldException( missingField = "epochSeconds", serialName = descriptor.serialName ) Instant.fromEpochSeconds(epochSeconds, nanosecondsOfSecond) } + @OptIn(ExperimentalSerializationApi::class) override fun serialize(encoder: Encoder, value: Instant) { encoder.encodeStructure(descriptor) { encodeLongElement(descriptor, 0, value.epochSeconds) diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt index e2bc5eae8..24e071e75 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt @@ -26,6 +26,14 @@ class InstantSerializationTest: JsonTestBase() { )) { assertJsonFormAndRestored(serializer, instant, json) } + for ((instant, json) in listOf( + Pair(Instant.fromEpochSeconds(987654321, 123456789), + "\"2001-04-19T07:55:21.123456789+03:30\""), + Pair(Instant.fromEpochSeconds(987654321, 123456789), + "\"2001-04-19T00:55:21.123456789-03:30\""), + )) { + assertRestoredFromJsonForm(serializer, json, instant) + } } private fun componentSerialization(serializer: KSerializer) { diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt index de8cfb38b..66f00177a 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/json/JsonTestBase.kt @@ -190,4 +190,15 @@ abstract class JsonTestBase { assertTrue("Failed with streaming = $jsonTestingMode\n\tsource value =$data\n\tdeserialized value=$deserialized") { check(data, deserialized) } } } + + internal fun assertRestoredFromJsonForm( + serializer: KSerializer, + jsonForm: String, + expected: T, + ) { + parametrizedTest { jsonTestingMode -> + val deserialized: T = Json.decodeFromString(serializer, jsonForm, jsonTestingMode) + assertEquals(expected, deserialized, "Failed with streaming = $jsonTestingMode") + } + } } From 5efb20f408ad7f1402b37f9d85efcc662dbcec70 Mon Sep 17 00:00:00 2001 From: Dmitry Khalanskiy Date: Wed, 30 Apr 2025 10:24:41 +0200 Subject: [PATCH 9/9] Check shouldEncodeElementDefault in InstantComponentSerializer --- .../builtins/InstantComponentSerializer.kt | 2 +- .../kotlinx/serialization/InstantSerializationTest.kt | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt b/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt index 4340a1181..3aaedccb9 100644 --- a/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt +++ b/core/commonMain/src/kotlinx/serialization/builtins/InstantComponentSerializer.kt @@ -52,7 +52,7 @@ public object InstantComponentSerializer : KSerializer { override fun serialize(encoder: Encoder, value: Instant) { encoder.encodeStructure(descriptor) { encodeLongElement(descriptor, 0, value.epochSeconds) - if (value.nanosecondsOfSecond != 0) { + if (value.nanosecondsOfSecond != 0 || shouldEncodeElementDefault(descriptor, 1)) { encodeIntElement(descriptor, 1, value.nanosecondsOfSecond) } } diff --git a/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt b/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt index 24e071e75..b15a01f66 100644 --- a/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt +++ b/formats/json-tests/commonTest/src/kotlinx/serialization/InstantSerializationTest.kt @@ -45,14 +45,21 @@ class InstantSerializationTest: JsonTestBase() { Pair(Instant.fromEpochSeconds(987654321, 123456789), "{\"epochSeconds\":987654321,\"nanosecondsOfSecond\":123456789}"), Pair(Instant.fromEpochSeconds(987654321, 0), - "{\"epochSeconds\":987654321}"), + "{\"epochSeconds\":987654321,\"nanosecondsOfSecond\":0}"), )) { assertJsonFormAndRestored(serializer, instant, json) } - // check that having a `"nanosecondsOfSecond": 0` field doesn't break deserialization + // by default, `nanosecondsOfSecond` is optional + assertJsonFormAndRestored(serializer, Instant.fromEpochSeconds(987654321, 0), + "{\"epochSeconds\":987654321}", Json { }) + // having a `"nanosecondsOfSecond": 0` field doesn't break deserialization assertEquals(Instant.fromEpochSeconds(987654321, 0), Json.decodeFromString(serializer, "{\"epochSeconds\":987654321,\"nanosecondsOfSecond\":0}")) + // as does not having a `"nanosecondsOfSecond"` field if `encodeDefaults` is true + assertEquals(Instant.fromEpochSeconds(987654321, 0), + default.decodeFromString(serializer, + "{\"epochSeconds\":987654321}")) // "epochSeconds" should always be present assertFailsWith { Json.decodeFromString(serializer, "{}") } assertFailsWith { Json.decodeFromString(serializer, "{\"nanosecondsOfSecond\":3}") }