diff --git a/core/common/src/serializers/InstantSerializers.kt b/core/common/src/serializers/InstantSerializers.kt index c64bdf47..01c21373 100644 --- a/core/common/src/serializers/InstantSerializers.kt +++ b/core/common/src/serializers/InstantSerializers.kt @@ -6,6 +6,9 @@ package kotlinx.datetime.serializers import kotlinx.datetime.Instant +import kotlinx.datetime.format +import kotlinx.datetime.format.DateTimeComponents +import kotlinx.datetime.format.DateTimeFormat import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -75,3 +78,38 @@ public object InstantComponentSerializer : KSerializer { } } + +/** + * An abstract serializer for [Instant] values that uses + * a custom [DateTimeFormat] for serializing to and deserializing. + * + * [format] should be a format that includes enough components to unambiguously define a date, a time, and a UTC offset. + * See [Instant.parse] for details of how deserialization is performed. + * + * When serializing, the [Instant] value is formatted as a string using the specified [format] + * in the [ZERO][UtcOffset.ZERO] UTC offset. + * + * This serializer is abstract and must be subclassed to provide a concrete serializer. + * Example: + * ``` + * object Rfc1123InstantSerializer : CustomInstantSerializer(DateTimeComponents.Formats.RFC_1123) + * ``` + * + * Note that [Instant] is [kotlinx.serialization.Serializable] by default, + * so it is not necessary to create custom serializers when the format is not important. + * Additionally, [InstantIso8601Serializer] is provided for the ISO 8601 format. + */ +public abstract class CustomInstantSerializer( + private val format: DateTimeFormat, +) : KSerializer { + + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("kotlinx.datetime.Instant", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): Instant = + Instant.parse(decoder.decodeString(), format) + + override fun serialize(encoder: Encoder, value: Instant) { + encoder.encodeString(value.format(format)) + } +} diff --git a/core/common/src/serializers/LocalDateSerializers.kt b/core/common/src/serializers/LocalDateSerializers.kt index e1c1c5e9..1b7f4f3b 100644 --- a/core/common/src/serializers/LocalDateSerializers.kt +++ b/core/common/src/serializers/LocalDateSerializers.kt @@ -6,6 +6,7 @@ package kotlinx.datetime.serializers import kotlinx.datetime.LocalDate +import kotlinx.datetime.format.DateTimeFormat import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -76,3 +77,33 @@ public object LocalDateComponentSerializer: KSerializer { } } + +/** + * An abstract serializer for [LocalDate] values that uses + * a custom [DateTimeFormat] to serialize and deserialize the value. + * + * This serializer is abstract and must be subclassed to provide a concrete serializer. + * Example: + * ``` + * object IsoBasicLocalDateSerializer : CustomLocalDateSerializer(LocalDate.Formats.ISO_BASIC) + * ``` + * + * Note that [LocalDate] is [kotlinx.serialization.Serializable] by default, + * so it is not necessary to create custom serializers when the format is not important. + * Additionally, [LocalDateIso8601Serializer] is provided for the ISO 8601 format. + */ +public abstract class CustomLocalDateSerializer( + format: DateTimeFormat, +) : KSerializer by format.asKSerializer("kotlinx.datetime.LocalDate") + +internal fun DateTimeFormat.asKSerializer(classFqn: String): KSerializer = + object : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor(classFqn, PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): T = parse(decoder.decodeString()) + + override fun serialize(encoder: Encoder, value: T) { + encoder.encodeString(format(value)) + } + } diff --git a/core/common/src/serializers/LocalDateTimeSerializers.kt b/core/common/src/serializers/LocalDateTimeSerializers.kt index 73f29115..f68c7d05 100644 --- a/core/common/src/serializers/LocalDateTimeSerializers.kt +++ b/core/common/src/serializers/LocalDateTimeSerializers.kt @@ -6,6 +6,7 @@ package kotlinx.datetime.serializers import kotlinx.datetime.* +import kotlinx.datetime.format.DateTimeFormat import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -98,3 +99,25 @@ public object LocalDateTimeComponentSerializer: KSerializer { } } + +/** + * An abstract serializer for [LocalDateTime] values that uses + * a custom [DateTimeFormat] to serialize and deserialize the value. + * + * This serializer is abstract and must be subclassed to provide a concrete serializer. + * Example: + * ``` + * object PythonDateTimeSerializer : CustomLocalDateTimeSerializer(LocalDateTime.Format { + * date(LocalDate.Formats.ISO) + * char(' ') + * time(LocalTime.Formats.ISO) + * }) + * ``` + * + * Note that [LocalDateTime] is [kotlinx.serialization.Serializable] by default, + * so it is not necessary to create custom serializers when the format is not important. + * Additionally, [LocalDateTimeIso8601Serializer] is provided for the ISO 8601 format. + */ +public abstract class CustomLocalDateTimeSerializer( + format: DateTimeFormat, +) : KSerializer by format.asKSerializer("kotlinx.datetime.LocalDateTime") diff --git a/core/common/src/serializers/LocalTimeSerializers.kt b/core/common/src/serializers/LocalTimeSerializers.kt index b8c1c0eb..90e034e7 100644 --- a/core/common/src/serializers/LocalTimeSerializers.kt +++ b/core/common/src/serializers/LocalTimeSerializers.kt @@ -6,6 +6,7 @@ package kotlinx.datetime.serializers import kotlinx.datetime.* +import kotlinx.datetime.format.DateTimeFormat import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -81,3 +82,23 @@ public object LocalTimeComponentSerializer : KSerializer { } } } + +/** + * An abstract serializer for [LocalTime] values that uses + * a custom [DateTimeFormat] to serialize and deserialize the value. + * + * This serializer is abstract and must be subclassed to provide a concrete serializer. + * Example: + * ``` + * object FixedWidthTimeSerializer : CustomLocalTimeSerializer(LocalTime.Format { + * hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(3) + * }) + * ``` + * + * Note that [LocalTime] is [kotlinx.serialization.Serializable] by default, + * so it is not necessary to create custom serializers when the format is not important. + * Additionally, [LocalTimeIso8601Serializer] is provided for the ISO 8601 format. + */ +public abstract class CustomLocalTimeSerializer( + format: DateTimeFormat, +) : KSerializer by format.asKSerializer("kotlinx.datetime.LocalTime") diff --git a/core/common/src/serializers/TimeZoneSerializers.kt b/core/common/src/serializers/TimeZoneSerializers.kt index 2d6d1c38..add5cc82 100644 --- a/core/common/src/serializers/TimeZoneSerializers.kt +++ b/core/common/src/serializers/TimeZoneSerializers.kt @@ -5,9 +5,8 @@ package kotlinx.datetime.serializers -import kotlinx.datetime.FixedOffsetTimeZone -import kotlinx.datetime.TimeZone -import kotlinx.datetime.UtcOffset +import kotlinx.datetime.* +import kotlinx.datetime.format.DateTimeFormat import kotlinx.serialization.* import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.* @@ -74,3 +73,21 @@ public object UtcOffsetSerializer: KSerializer { } } + +/** + * An abstract serializer for [UtcOffset] values that uses + * a custom [DateTimeFormat] to serialize and deserialize the value. + * + * This serializer is abstract and must be subclassed to provide a concrete serializer. + * Example: + * ``` + * object FourDigitOffsetSerializer : CustomUtcOffsetSerializer(UtcOffset.Formats.FOUR_DIGITS) + * ``` + * + * Note that [UtcOffset] is [kotlinx.serialization.Serializable] by default, + * so it is not necessary to create custom serializers when the format is not important. + * Additionally, [UtcOffsetSerializer] is provided for the ISO 8601 format. + */ +public abstract class CustomUtcOffsetSerializer( + format: DateTimeFormat, +) : KSerializer by format.asKSerializer("kotlinx.datetime.UtcOffset") diff --git a/serialization/common/test/InstantSerializationTest.kt b/serialization/common/test/InstantSerializationTest.kt index dea5c2db..564f609d 100644 --- a/serialization/common/test/InstantSerializationTest.kt +++ b/serialization/common/test/InstantSerializationTest.kt @@ -5,6 +5,7 @@ package kotlinx.datetime.serialization.test import kotlinx.datetime.* +import kotlinx.datetime.format.DateTimeComponents import kotlinx.datetime.serializers.* import kotlinx.serialization.* import kotlinx.serialization.json.* @@ -66,4 +67,25 @@ class InstantSerializationTest { // should be the same as the ISO 8601 iso8601Serialization(Json.serializersModule.serializer()) } + + object Rfc1123InstantSerializer : CustomInstantSerializer(DateTimeComponents.Formats.RFC_1123) + + @Test + fun testCustomSerializer() { + for ((instant, json) in listOf( + Pair(Instant.fromEpochSeconds(1607505416), + "\"Wed, 9 Dec 2020 09:16:56 GMT\""), + Pair(Instant.fromEpochSeconds(-1607505416), + "\"Thu, 23 Jan 1919 14:43:04 GMT\""), + Pair(Instant.fromEpochSeconds(987654321), + "\"Thu, 19 Apr 2001 04:25:21 GMT\""), + )) { + assertEquals(json, Json.encodeToString(Rfc1123InstantSerializer, instant)) + assertEquals(instant, Json.decodeFromString(Rfc1123InstantSerializer, json)) + } + assertEquals("\"Thu, 19 Apr 2001 04:25:21 GMT\"", + Json.encodeToString(Rfc1123InstantSerializer, Instant.fromEpochSeconds(987654321, 123456789))) + assertEquals(Instant.fromEpochSeconds(987654321), + Json.decodeFromString(Rfc1123InstantSerializer, "\"Thu, 19 Apr 2001 08:25:21 +0400\"")) + } } diff --git a/serialization/common/test/LocalDateSerializationTest.kt b/serialization/common/test/LocalDateSerializationTest.kt index 91ed3827..d4e547cf 100644 --- a/serialization/common/test/LocalDateSerializationTest.kt +++ b/serialization/common/test/LocalDateSerializationTest.kt @@ -70,4 +70,17 @@ class LocalDateSerializationTest { iso8601Serialization(Json.serializersModule.serializer()) } + object IsoBasicLocalDateSerializer : CustomLocalDateSerializer(LocalDate.Formats.ISO_BASIC) + + @Test + fun testCustomSerializer() { + for ((localDate, json) in listOf( + Pair(LocalDate(2020, 12, 9), "\"20201209\""), + Pair(LocalDate(-2020, 1, 1), "\"-20200101\""), + Pair(LocalDate(2019, 10, 1), "\"20191001\""), + )) { + assertEquals(json, Json.encodeToString(IsoBasicLocalDateSerializer, localDate)) + assertEquals(localDate, Json.decodeFromString(IsoBasicLocalDateSerializer, json)) + } + } } diff --git a/serialization/common/test/LocalDateTimeSerializationTest.kt b/serialization/common/test/LocalDateTimeSerializationTest.kt index c01e647c..7f6030b2 100644 --- a/serialization/common/test/LocalDateTimeSerializationTest.kt +++ b/serialization/common/test/LocalDateTimeSerializationTest.kt @@ -6,6 +6,7 @@ package kotlinx.datetime.serialization.test import kotlinx.datetime.* +import kotlinx.datetime.format.char import kotlinx.datetime.serializers.* import kotlinx.serialization.KSerializer import kotlinx.serialization.json.* @@ -82,4 +83,24 @@ class LocalDateTimeSerializationTest { // should be the same as the ISO 8601 iso8601Serialization(Json.serializersModule.serializer()) } + + object PythonDateTimeSerializer : CustomLocalDateTimeSerializer(LocalDateTime.Format { + date(LocalDate.Formats.ISO) + char(' ') + time(LocalTime.Formats.ISO) + }) + + @Test + fun testCustomSerializer() { + for ((localDateTime, json) in listOf( + Pair(LocalDateTime(2008, 7, 5, 2, 1), "\"2008-07-05 02:01:00\""), + Pair(LocalDateTime(2007, 12, 31, 23, 59, 1), "\"2007-12-31 23:59:01\""), + Pair(LocalDateTime(999, 12, 31, 23, 59, 59, 990000000), "\"0999-12-31 23:59:59.99\""), + Pair(LocalDateTime(-1, 1, 2, 23, 59, 59, 999990000), "\"-0001-01-02 23:59:59.99999\""), + Pair(LocalDateTime(-2008, 1, 2, 23, 59, 59, 999999990), "\"-2008-01-02 23:59:59.99999999\""), + )) { + assertEquals(json, Json.encodeToString(PythonDateTimeSerializer, localDateTime)) + assertEquals(localDateTime, Json.decodeFromString(PythonDateTimeSerializer, json)) + } + } } diff --git a/serialization/common/test/LocalTimeSerializationTest.kt b/serialization/common/test/LocalTimeSerializationTest.kt index 5df81f54..4e4d3847 100644 --- a/serialization/common/test/LocalTimeSerializationTest.kt +++ b/serialization/common/test/LocalTimeSerializationTest.kt @@ -6,6 +6,7 @@ package kotlinx.datetime.serialization.test import kotlinx.datetime.* +import kotlinx.datetime.format.char import kotlinx.datetime.serializers.* import kotlinx.serialization.KSerializer import kotlinx.serialization.json.* @@ -72,4 +73,23 @@ class LocalTimeSerializationTest { // should be the same as the ISO 8601 iso8601Serialization(Json.serializersModule.serializer()) } + + object FixedWidthTimeSerializer : CustomLocalTimeSerializer(LocalTime.Format { + hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(3) + }) + + @Test + fun testCustomSerializer() { + for ((localTime, json) in listOf( + Pair(LocalTime(2, 1), "\"02:01:00.000\""), + Pair(LocalTime(23, 59, 1), "\"23:59:01.000\""), + Pair(LocalTime(23, 59, 59, 990000000), "\"23:59:59.990\""), + Pair(LocalTime(23, 59, 59, 999000000), "\"23:59:59.999\""), + )) { + assertEquals(json, Json.encodeToString(FixedWidthTimeSerializer, localTime)) + assertEquals(localTime, Json.decodeFromString(FixedWidthTimeSerializer, json)) + } + assertEquals("\"12:34:56.123\"", Json.encodeToString(FixedWidthTimeSerializer, + LocalTime(12, 34, 56, 123999999))) + } } diff --git a/serialization/common/test/UtcOffsetSerializationTest.kt b/serialization/common/test/UtcOffsetSerializationTest.kt index 2504d6fb..a1b053cb 100644 --- a/serialization/common/test/UtcOffsetSerializationTest.kt +++ b/serialization/common/test/UtcOffsetSerializationTest.kt @@ -35,4 +35,20 @@ class UtcOffsetSerializationTest { testSerializationAsPrimitive(UtcOffsetSerializer) testSerializationAsPrimitive(UtcOffset.serializer()) } + + object FourDigitOffsetSerializer : CustomUtcOffsetSerializer(UtcOffset.Formats.FOUR_DIGITS) + + @Test + fun testCustomSerializer() { + for ((utcOffset, json) in listOf( + Pair(UtcOffset.ZERO, "\"+0000\""), + Pair(UtcOffset(2), "\"+0200\""), + Pair(UtcOffset(2, 30), "\"+0230\""), + Pair(UtcOffset(-2, -30), "\"-0230\""), + )) { + assertEquals(json, Json.encodeToString(FourDigitOffsetSerializer, utcOffset)) + assertEquals(utcOffset, Json.decodeFromString(FourDigitOffsetSerializer, json)) + } + assertEquals("\"+1234\"", Json.encodeToString(FourDigitOffsetSerializer, UtcOffset(12, 34, 56))) + } }