Skip to content

Commit 314121a

Browse files
committed
Provide custom serializers based on DateTimeFormat
The names of the serializers are still under discussion, but it's already decided that the serializers are going to be abstract classes. Fixes #350
1 parent 29276fe commit 314121a

10 files changed

+225
-3
lines changed

Diff for: core/common/src/serializers/InstantSerializers.kt

+38
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
package kotlinx.datetime.serializers
77

88
import kotlinx.datetime.Instant
9+
import kotlinx.datetime.format
10+
import kotlinx.datetime.format.DateTimeComponents
11+
import kotlinx.datetime.format.DateTimeFormat
912
import kotlinx.serialization.*
1013
import kotlinx.serialization.descriptors.*
1114
import kotlinx.serialization.encoding.*
@@ -75,3 +78,38 @@ public object InstantComponentSerializer : KSerializer<Instant> {
7578
}
7679

7780
}
81+
82+
/**
83+
* An abstract serializer for [Instant] values that uses
84+
* a custom [DateTimeFormat] for serializing to and deserializing.
85+
*
86+
* [format] should be a format that includes enough components to unambiguously define a date, a time, and a UTC offset.
87+
* See [Instant.parse] for details of how deserialization is performed.
88+
*
89+
* When serializing, the [Instant] value is formatted as a string using the specified [format]
90+
* in the [ZERO][UtcOffset.ZERO] UTC offset.
91+
*
92+
* This serializer is abstract and must be subclassed to provide a concrete serializer.
93+
* Example:
94+
* ```
95+
* object Rfc1123InstantSerializer : CustomInstantSerializer(DateTimeComponents.Formats.RFC_1123)
96+
* ```
97+
*
98+
* Note that [Instant] is [kotlinx.serialization.Serializable] by default,
99+
* so it is not necessary to create custom serializers when the format is not important.
100+
* Additionally, [InstantIso8601Serializer] is provided for the ISO 8601 format.
101+
*/
102+
public abstract class CustomInstantSerializer(
103+
private val format: DateTimeFormat<DateTimeComponents>,
104+
) : KSerializer<Instant> {
105+
106+
override val descriptor: SerialDescriptor =
107+
PrimitiveSerialDescriptor("kotlinx.datetime.Instant", PrimitiveKind.STRING)
108+
109+
override fun deserialize(decoder: Decoder): Instant =
110+
Instant.parse(decoder.decodeString(), format)
111+
112+
override fun serialize(encoder: Encoder, value: Instant) {
113+
encoder.encodeString(value.format(format))
114+
}
115+
}

Diff for: core/common/src/serializers/LocalDateSerializers.kt

+31
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package kotlinx.datetime.serializers
77

88
import kotlinx.datetime.LocalDate
9+
import kotlinx.datetime.format.DateTimeFormat
910
import kotlinx.serialization.*
1011
import kotlinx.serialization.descriptors.*
1112
import kotlinx.serialization.encoding.*
@@ -76,3 +77,33 @@ public object LocalDateComponentSerializer: KSerializer<LocalDate> {
7677
}
7778

7879
}
80+
81+
/**
82+
* An abstract serializer for [LocalDate] values that uses
83+
* a custom [DateTimeFormat] to serialize and deserialize the value.
84+
*
85+
* This serializer is abstract and must be subclassed to provide a concrete serializer.
86+
* Example:
87+
* ```
88+
* object IsoBasicLocalDateSerializer : CustomLocalDateSerializer(LocalDate.Formats.ISO_BASIC)
89+
* ```
90+
*
91+
* Note that [LocalDate] is [kotlinx.serialization.Serializable] by default,
92+
* so it is not necessary to create custom serializers when the format is not important.
93+
* Additionally, [LocalDateIso8601Serializer] is provided for the ISO 8601 format.
94+
*/
95+
public abstract class CustomLocalDateSerializer(
96+
format: DateTimeFormat<LocalDate>,
97+
) : KSerializer<LocalDate> by format.asKSerializer("kotlinx.datetime.LocalDate")
98+
99+
internal fun <T> DateTimeFormat<T>.asKSerializer(classFqn: String): KSerializer<T> =
100+
object : KSerializer<T> {
101+
override val descriptor: SerialDescriptor =
102+
PrimitiveSerialDescriptor(classFqn, PrimitiveKind.STRING)
103+
104+
override fun deserialize(decoder: Decoder): T = parse(decoder.decodeString())
105+
106+
override fun serialize(encoder: Encoder, value: T) {
107+
encoder.encodeString(format(value))
108+
}
109+
}

Diff for: core/common/src/serializers/LocalDateTimeSerializers.kt

+23
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package kotlinx.datetime.serializers
77

88
import kotlinx.datetime.*
9+
import kotlinx.datetime.format.DateTimeFormat
910
import kotlinx.serialization.*
1011
import kotlinx.serialization.descriptors.*
1112
import kotlinx.serialization.encoding.*
@@ -98,3 +99,25 @@ public object LocalDateTimeComponentSerializer: KSerializer<LocalDateTime> {
9899
}
99100

100101
}
102+
103+
/**
104+
* An abstract serializer for [LocalDateTime] values that uses
105+
* a custom [DateTimeFormat] to serialize and deserialize the value.
106+
*
107+
* This serializer is abstract and must be subclassed to provide a concrete serializer.
108+
* Example:
109+
* ```
110+
* object PythonDateTimeSerializer : CustomLocalDateTimeSerializer(LocalDateTime.Format {
111+
* date(LocalDate.Formats.ISO)
112+
* char(' ')
113+
* time(LocalTime.Formats.ISO)
114+
* })
115+
* ```
116+
*
117+
* Note that [LocalDateTime] is [kotlinx.serialization.Serializable] by default,
118+
* so it is not necessary to create custom serializers when the format is not important.
119+
* Additionally, [LocalDateTimeIso8601Serializer] is provided for the ISO 8601 format.
120+
*/
121+
public abstract class CustomLocalDateTimeSerializer(
122+
format: DateTimeFormat<LocalDateTime>,
123+
) : KSerializer<LocalDateTime> by format.asKSerializer("kotlinx.datetime.LocalDateTime")

Diff for: core/common/src/serializers/LocalTimeSerializers.kt

+21
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package kotlinx.datetime.serializers
77

88
import kotlinx.datetime.*
9+
import kotlinx.datetime.format.DateTimeFormat
910
import kotlinx.serialization.*
1011
import kotlinx.serialization.descriptors.*
1112
import kotlinx.serialization.encoding.*
@@ -81,3 +82,23 @@ public object LocalTimeComponentSerializer : KSerializer<LocalTime> {
8182
}
8283
}
8384
}
85+
86+
/**
87+
* An abstract serializer for [LocalTime] values that uses
88+
* a custom [DateTimeFormat] to serialize and deserialize the value.
89+
*
90+
* This serializer is abstract and must be subclassed to provide a concrete serializer.
91+
* Example:
92+
* ```
93+
* object FixedWidthTimeSerializer : CustomLocalTimeSerializer(LocalTime.Format {
94+
* hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(3)
95+
* })
96+
* ```
97+
*
98+
* Note that [LocalTime] is [kotlinx.serialization.Serializable] by default,
99+
* so it is not necessary to create custom serializers when the format is not important.
100+
* Additionally, [LocalTimeIso8601Serializer] is provided for the ISO 8601 format.
101+
*/
102+
public abstract class CustomLocalTimeSerializer(
103+
format: DateTimeFormat<LocalTime>,
104+
) : KSerializer<LocalTime> by format.asKSerializer("kotlinx.datetime.LocalTime")

Diff for: core/common/src/serializers/TimeZoneSerializers.kt

+20-3
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55

66
package kotlinx.datetime.serializers
77

8-
import kotlinx.datetime.FixedOffsetTimeZone
9-
import kotlinx.datetime.TimeZone
10-
import kotlinx.datetime.UtcOffset
8+
import kotlinx.datetime.*
9+
import kotlinx.datetime.format.DateTimeFormat
1110
import kotlinx.serialization.*
1211
import kotlinx.serialization.descriptors.*
1312
import kotlinx.serialization.encoding.*
@@ -74,3 +73,21 @@ public object UtcOffsetSerializer: KSerializer<UtcOffset> {
7473
}
7574

7675
}
76+
77+
/**
78+
* An abstract serializer for [UtcOffset] values that uses
79+
* a custom [DateTimeFormat] to serialize and deserialize the value.
80+
*
81+
* This serializer is abstract and must be subclassed to provide a concrete serializer.
82+
* Example:
83+
* ```
84+
* object FourDigitOffsetSerializer : CustomUtcOffsetSerializer(UtcOffset.Formats.FOUR_DIGITS)
85+
* ```
86+
*
87+
* Note that [UtcOffset] is [kotlinx.serialization.Serializable] by default,
88+
* so it is not necessary to create custom serializers when the format is not important.
89+
* Additionally, [UtcOffsetSerializer] is provided for the ISO 8601 format.
90+
*/
91+
public abstract class CustomUtcOffsetSerializer(
92+
format: DateTimeFormat<UtcOffset>,
93+
) : KSerializer<UtcOffset> by format.asKSerializer("kotlinx.datetime.UtcOffset")

Diff for: serialization/common/test/InstantSerializationTest.kt

+22
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package kotlinx.datetime.serialization.test
66

77
import kotlinx.datetime.*
8+
import kotlinx.datetime.format.DateTimeComponents
89
import kotlinx.datetime.serializers.*
910
import kotlinx.serialization.*
1011
import kotlinx.serialization.json.*
@@ -66,4 +67,25 @@ class InstantSerializationTest {
6667
// should be the same as the ISO 8601
6768
iso8601Serialization(Json.serializersModule.serializer())
6869
}
70+
71+
object Rfc1123InstantSerializer : CustomInstantSerializer(DateTimeComponents.Formats.RFC_1123)
72+
73+
@Test
74+
fun testCustomSerializer() {
75+
for ((instant, json) in listOf(
76+
Pair(Instant.fromEpochSeconds(1607505416),
77+
"\"Wed, 9 Dec 2020 09:16:56 GMT\""),
78+
Pair(Instant.fromEpochSeconds(-1607505416),
79+
"\"Thu, 23 Jan 1919 14:43:04 GMT\""),
80+
Pair(Instant.fromEpochSeconds(987654321),
81+
"\"Thu, 19 Apr 2001 04:25:21 GMT\""),
82+
)) {
83+
assertEquals(json, Json.encodeToString(Rfc1123InstantSerializer, instant))
84+
assertEquals(instant, Json.decodeFromString(Rfc1123InstantSerializer, json))
85+
}
86+
assertEquals("\"Thu, 19 Apr 2001 04:25:21 GMT\"",
87+
Json.encodeToString(Rfc1123InstantSerializer, Instant.fromEpochSeconds(987654321, 123456789)))
88+
assertEquals(Instant.fromEpochSeconds(987654321),
89+
Json.decodeFromString(Rfc1123InstantSerializer, "\"Thu, 19 Apr 2001 08:25:21 +0400\""))
90+
}
6991
}

Diff for: serialization/common/test/LocalDateSerializationTest.kt

+13
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,17 @@ class LocalDateSerializationTest {
7070
iso8601Serialization(Json.serializersModule.serializer())
7171
}
7272

73+
object IsoBasicLocalDateSerializer : CustomLocalDateSerializer(LocalDate.Formats.ISO_BASIC)
74+
75+
@Test
76+
fun testCustomSerializer() {
77+
for ((localDate, json) in listOf(
78+
Pair(LocalDate(2020, 12, 9), "\"20201209\""),
79+
Pair(LocalDate(-2020, 1, 1), "\"-20200101\""),
80+
Pair(LocalDate(2019, 10, 1), "\"20191001\""),
81+
)) {
82+
assertEquals(json, Json.encodeToString(IsoBasicLocalDateSerializer, localDate))
83+
assertEquals(localDate, Json.decodeFromString(IsoBasicLocalDateSerializer, json))
84+
}
85+
}
7386
}

Diff for: serialization/common/test/LocalDateTimeSerializationTest.kt

+21
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package kotlinx.datetime.serialization.test
77

88
import kotlinx.datetime.*
9+
import kotlinx.datetime.format.char
910
import kotlinx.datetime.serializers.*
1011
import kotlinx.serialization.KSerializer
1112
import kotlinx.serialization.json.*
@@ -82,4 +83,24 @@ class LocalDateTimeSerializationTest {
8283
// should be the same as the ISO 8601
8384
iso8601Serialization(Json.serializersModule.serializer())
8485
}
86+
87+
object PythonDateTimeSerializer : CustomLocalDateTimeSerializer(LocalDateTime.Format {
88+
date(LocalDate.Formats.ISO)
89+
char(' ')
90+
time(LocalTime.Formats.ISO)
91+
})
92+
93+
@Test
94+
fun testCustomSerializer() {
95+
for ((localDateTime, json) in listOf(
96+
Pair(LocalDateTime(2008, 7, 5, 2, 1), "\"2008-07-05 02:01:00\""),
97+
Pair(LocalDateTime(2007, 12, 31, 23, 59, 1), "\"2007-12-31 23:59:01\""),
98+
Pair(LocalDateTime(999, 12, 31, 23, 59, 59, 990000000), "\"0999-12-31 23:59:59.99\""),
99+
Pair(LocalDateTime(-1, 1, 2, 23, 59, 59, 999990000), "\"-0001-01-02 23:59:59.99999\""),
100+
Pair(LocalDateTime(-2008, 1, 2, 23, 59, 59, 999999990), "\"-2008-01-02 23:59:59.99999999\""),
101+
)) {
102+
assertEquals(json, Json.encodeToString(PythonDateTimeSerializer, localDateTime))
103+
assertEquals(localDateTime, Json.decodeFromString(PythonDateTimeSerializer, json))
104+
}
105+
}
85106
}

Diff for: serialization/common/test/LocalTimeSerializationTest.kt

+20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package kotlinx.datetime.serialization.test
77

88
import kotlinx.datetime.*
9+
import kotlinx.datetime.format.char
910
import kotlinx.datetime.serializers.*
1011
import kotlinx.serialization.KSerializer
1112
import kotlinx.serialization.json.*
@@ -72,4 +73,23 @@ class LocalTimeSerializationTest {
7273
// should be the same as the ISO 8601
7374
iso8601Serialization(Json.serializersModule.serializer())
7475
}
76+
77+
object FixedWidthTimeSerializer : CustomLocalTimeSerializer(LocalTime.Format {
78+
hour(); char(':'); minute(); char(':'); second(); char('.'); secondFraction(3)
79+
})
80+
81+
@Test
82+
fun testCustomSerializer() {
83+
for ((localTime, json) in listOf(
84+
Pair(LocalTime(2, 1), "\"02:01:00.000\""),
85+
Pair(LocalTime(23, 59, 1), "\"23:59:01.000\""),
86+
Pair(LocalTime(23, 59, 59, 990000000), "\"23:59:59.990\""),
87+
Pair(LocalTime(23, 59, 59, 999000000), "\"23:59:59.999\""),
88+
)) {
89+
assertEquals(json, Json.encodeToString(FixedWidthTimeSerializer, localTime))
90+
assertEquals(localTime, Json.decodeFromString(FixedWidthTimeSerializer, json))
91+
}
92+
assertEquals("\"12:34:56.123\"", Json.encodeToString(FixedWidthTimeSerializer,
93+
LocalTime(12, 34, 56, 123999999)))
94+
}
7595
}

Diff for: serialization/common/test/UtcOffsetSerializationTest.kt

+16
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,20 @@ class UtcOffsetSerializationTest {
3535
testSerializationAsPrimitive(UtcOffsetSerializer)
3636
testSerializationAsPrimitive(UtcOffset.serializer())
3737
}
38+
39+
object FourDigitOffsetSerializer : CustomUtcOffsetSerializer(UtcOffset.Formats.FOUR_DIGITS)
40+
41+
@Test
42+
fun testCustomSerializer() {
43+
for ((utcOffset, json) in listOf(
44+
Pair(UtcOffset.ZERO, "\"+0000\""),
45+
Pair(UtcOffset(2), "\"+0200\""),
46+
Pair(UtcOffset(2, 30), "\"+0230\""),
47+
Pair(UtcOffset(-2, -30), "\"-0230\""),
48+
)) {
49+
assertEquals(json, Json.encodeToString(FourDigitOffsetSerializer, utcOffset))
50+
assertEquals(utcOffset, Json.decodeFromString(FourDigitOffsetSerializer, json))
51+
}
52+
assertEquals("\"+1234\"", Json.encodeToString(FourDigitOffsetSerializer, UtcOffset(12, 34, 56)))
53+
}
3854
}

0 commit comments

Comments
 (0)