Skip to content

Commit e803d80

Browse files
committed
Implement java.io.Serializable for some of the classes
Implement java.io.Serializable for * Instant * LocalDate * LocalTime * LocalDateTime * UtcOffset TimeZone is not `Serializable` because its behavior is system-dependent. We can make it `java.io.Serializable` later if there is demand. We are using string representations instead of relying on Java's entities being `java.io.Serializable` so that we have more freedom to change our implementation later. Fixes #143
1 parent 02e4e4d commit e803d80

File tree

6 files changed

+159
-6
lines changed

6 files changed

+159
-6
lines changed

core/jvm/src/Instant.kt

+23-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import kotlinx.datetime.internal.*
1212
import kotlinx.datetime.serializers.InstantIso8601Serializer
1313
import kotlinx.serialization.Serializable
1414
import java.time.DateTimeException
15+
import java.time.LocalDate
1516
import java.time.format.DateTimeParseException
1617
import java.time.temporal.ChronoUnit
1718
import kotlin.time.*
@@ -22,7 +23,9 @@ import java.time.OffsetDateTime as jtOffsetDateTime
2223
import java.time.Clock as jtClock
2324

2425
@Serializable(with = InstantIso8601Serializer::class)
25-
public actual class Instant internal constructor(internal val value: jtInstant) : Comparable<Instant> {
26+
public actual class Instant internal constructor(
27+
internal val value: jtInstant
28+
) : Comparable<Instant>, java.io.Serializable {
2629

2730
public actual val epochSeconds: Long
2831
get() = value.epochSecond
@@ -111,6 +114,25 @@ public actual class Instant internal constructor(internal val value: jtInstant)
111114

112115
internal actual val MIN: Instant = Instant(jtInstant.MIN)
113116
internal actual val MAX: Instant = Instant(jtInstant.MAX)
117+
118+
@JvmStatic
119+
private val serialVersionUID: Long = 1L
120+
}
121+
122+
private fun writeObject(oStream: java.io.ObjectOutputStream) {
123+
oStream.defaultWriteObject()
124+
oStream.writeObject(value.toString())
125+
}
126+
127+
private fun readObject(iStream: java.io.ObjectInputStream) {
128+
iStream.defaultReadObject()
129+
val field = this::class.java.getDeclaredField(::value.name)
130+
field.isAccessible = true
131+
field.set(this, jtOffsetDateTime.parse(fixOffsetRepresentation(iStream.readObject() as String)).toInstant())
132+
}
133+
134+
private fun readObjectNoData() {
135+
throw java.io.InvalidObjectException("Stream data required")
114136
}
115137
}
116138

core/jvm/src/LocalDate.kt

+22-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ import java.time.temporal.ChronoUnit
1717
import java.time.LocalDate as jtLocalDate
1818

1919
@Serializable(with = LocalDateIso8601Serializer::class)
20-
public actual class LocalDate internal constructor(internal val value: jtLocalDate) : Comparable<LocalDate> {
20+
public actual class LocalDate internal constructor(
21+
internal val value: jtLocalDate
22+
) : Comparable<LocalDate>, java.io.Serializable {
2123
public actual companion object {
2224
public actual fun parse(input: CharSequence, format: DateTimeFormat<LocalDate>): LocalDate =
2325
if (format === Formats.ISO) {
@@ -42,6 +44,9 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
4244
@Suppress("FunctionName")
4345
public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat<LocalDate> =
4446
LocalDateFormat.build(block)
47+
48+
@JvmStatic
49+
private val serialVersionUID: Long = 1L
4550
}
4651

4752
public actual object Formats {
@@ -76,6 +81,22 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
7681
actual override fun compareTo(other: LocalDate): Int = this.value.compareTo(other.value)
7782

7883
public actual fun toEpochDays(): Int = value.toEpochDay().clampToInt()
84+
85+
private fun writeObject(oStream: java.io.ObjectOutputStream) {
86+
oStream.defaultWriteObject()
87+
oStream.writeObject(value.toString())
88+
}
89+
90+
private fun readObject(iStream: java.io.ObjectInputStream) {
91+
iStream.defaultReadObject()
92+
val field = this::class.java.getDeclaredField(::value.name)
93+
field.isAccessible = true
94+
field.set(this, jtLocalDate.parse(iStream.readObject() as String))
95+
}
96+
97+
private fun readObjectNoData() {
98+
throw java.io.InvalidObjectException("Stream data required")
99+
}
79100
}
80101

81102
@Deprecated("Use the plus overload with an explicit number of units", ReplaceWith("this.plus(1, unit)"))

core/jvm/src/LocalDateTime.kt

+22-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ public actual typealias Month = java.time.Month
1616
public actual typealias DayOfWeek = java.time.DayOfWeek
1717

1818
@Serializable(with = LocalDateTimeIso8601Serializer::class)
19-
public actual class LocalDateTime internal constructor(internal val value: jtLocalDateTime) : Comparable<LocalDateTime> {
19+
public actual class LocalDateTime internal constructor(
20+
// only a `var` to allow Java deserialization
21+
internal var value: jtLocalDateTime
22+
) : Comparable<LocalDateTime>, java.io.Serializable {
2023

2124
public actual constructor(year: Int, monthNumber: Int, dayOfMonth: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) :
2225
this(try {
@@ -77,11 +80,29 @@ public actual class LocalDateTime internal constructor(internal val value: jtLoc
7780
@Suppress("FunctionName")
7881
public actual fun Format(builder: DateTimeFormatBuilder.WithDateTime.() -> Unit): DateTimeFormat<LocalDateTime> =
7982
LocalDateTimeFormat.build(builder)
83+
84+
@JvmStatic
85+
private val serialVersionUID: Long = 1L
8086
}
8187

8288
public actual object Formats {
8389
public actual val ISO: DateTimeFormat<LocalDateTime> = ISO_DATETIME
8490
}
8591

92+
private fun writeObject(oStream: java.io.ObjectOutputStream) {
93+
oStream.defaultWriteObject()
94+
oStream.writeObject(value.toString())
95+
}
96+
97+
private fun readObject(iStream: java.io.ObjectInputStream) {
98+
iStream.defaultReadObject()
99+
val field = this::class.java.getDeclaredField(::value.name)
100+
field.isAccessible = true
101+
field.set(this, jtLocalDateTime.parse(iStream.readObject() as String))
102+
}
103+
104+
private fun readObjectNoData() {
105+
throw java.io.InvalidObjectException("Stream data required")
106+
}
86107
}
87108

core/jvm/src/LocalTime.kt

+23-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import java.time.format.DateTimeParseException
1515
import java.time.LocalTime as jtLocalTime
1616

1717
@Serializable(with = LocalTimeIso8601Serializer::class)
18-
public actual class LocalTime internal constructor(internal val value: jtLocalTime) :
19-
Comparable<LocalTime> {
18+
public actual class LocalTime internal constructor(
19+
// only a `var` to allow Java deserialization
20+
internal var value: jtLocalTime
21+
) : Comparable<LocalTime>, java.io.Serializable {
2022

2123
public actual constructor(hour: Int, minute: Int, second: Int, nanosecond: Int) :
2224
this(
@@ -83,10 +85,29 @@ public actual class LocalTime internal constructor(internal val value: jtLocalTi
8385
@Suppress("FunctionName")
8486
public actual fun Format(builder: DateTimeFormatBuilder.WithTime.() -> Unit): DateTimeFormat<LocalTime> =
8587
LocalTimeFormat.build(builder)
88+
89+
@JvmStatic
90+
private val serialVersionUID: Long = 1L
8691
}
8792

8893
public actual object Formats {
8994
public actual val ISO: DateTimeFormat<LocalTime> get() = ISO_TIME
9095

9196
}
97+
98+
private fun writeObject(oStream: java.io.ObjectOutputStream) {
99+
oStream.defaultWriteObject()
100+
oStream.writeObject(value.toString())
101+
}
102+
103+
private fun readObject(iStream: java.io.ObjectInputStream) {
104+
iStream.defaultReadObject()
105+
val field = this::class.java.getDeclaredField(::value.name)
106+
field.isAccessible = true
107+
field.set(this, jtLocalTime.parse(iStream.readObject() as String))
108+
}
109+
110+
private fun readObjectNoData() {
111+
throw java.io.InvalidObjectException("Stream data required")
112+
}
92113
}

core/jvm/src/UtcOffsetJvm.kt

+19-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ import java.time.format.DateTimeFormatterBuilder
1414
import java.time.format.*
1515

1616
@Serializable(with = UtcOffsetSerializer::class)
17-
public actual class UtcOffset(internal val zoneOffset: ZoneOffset) {
17+
public actual class UtcOffset(
18+
internal val zoneOffset: ZoneOffset
19+
): java.io.Serializable {
1820
public actual val totalSeconds: Int get() = zoneOffset.totalSeconds
1921

2022
override fun hashCode(): Int = zoneOffset.hashCode()
@@ -44,6 +46,22 @@ public actual class UtcOffset(internal val zoneOffset: ZoneOffset) {
4446
public actual val ISO_BASIC: DateTimeFormat<UtcOffset> get() = ISO_OFFSET_BASIC
4547
public actual val FOUR_DIGITS: DateTimeFormat<UtcOffset> get() = FOUR_DIGIT_OFFSET
4648
}
49+
50+
private fun writeObject(oStream: java.io.ObjectOutputStream) {
51+
oStream.defaultWriteObject()
52+
oStream.writeObject(zoneOffset.toString())
53+
}
54+
55+
private fun readObject(iStream: java.io.ObjectInputStream) {
56+
iStream.defaultReadObject()
57+
val field = this::class.java.getDeclaredField(::zoneOffset.name)
58+
field.isAccessible = true
59+
field.set(this, ZoneOffset.of(iStream.readObject() as String))
60+
}
61+
62+
private fun readObjectNoData() {
63+
throw java.io.InvalidObjectException("Stream data required")
64+
}
4765
}
4866

4967
@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")

core/jvm/test/JvmSerializationTest.kt

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2019-2024 JetBrains s.r.o. and contributors.
3+
* Use of this source code is governed by the Apache 2.0 License that can be found in the LICENSE.txt file.
4+
*/
5+
6+
package kotlinx.datetime
7+
8+
import java.io.*
9+
import kotlin.test.*
10+
11+
class JvmSerializationTest {
12+
13+
@Test
14+
fun serializeInstant() {
15+
roundTripSerialization(Instant.fromEpochSeconds(1234567890, 123456789))
16+
}
17+
18+
@Test
19+
fun serializeLocalTime() {
20+
roundTripSerialization(LocalTime(12, 34, 56, 789))
21+
}
22+
23+
@Test
24+
fun serializeLocalDateTime() {
25+
roundTripSerialization(LocalDateTime(2022, 1, 23, 21, 35, 53, 125_123_612))
26+
}
27+
28+
@Test
29+
fun serializeUtcOffset() {
30+
roundTripSerialization(UtcOffset(hours = 3, minutes = 30, seconds = 15))
31+
}
32+
33+
@Test
34+
fun serializeTimeZone() {
35+
assertFailsWith<NotSerializableException> {
36+
roundTripSerialization(TimeZone.of("Europe/Moscow"))
37+
}
38+
}
39+
40+
private fun <T> roundTripSerialization(value: T) {
41+
val bos = ByteArrayOutputStream()
42+
val oos = ObjectOutputStream(bos)
43+
oos.writeObject(value)
44+
val serialized = bos.toByteArray()
45+
val bis = ByteArrayInputStream(serialized)
46+
ObjectInputStream(bis).use { ois ->
47+
assertEquals(value, ois.readObject())
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)