Skip to content

Commit 9ec1061

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 b37b329 commit 9ec1061

File tree

6 files changed

+158
-6
lines changed

6 files changed

+158
-6
lines changed

core/jvm/src/Instant.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ import java.time.Instant as jtInstant
2020
import java.time.Clock as jtClock
2121

2222
@Serializable(with = InstantIso8601Serializer::class)
23-
public actual class Instant internal constructor(internal val value: jtInstant) : Comparable<Instant> {
23+
public actual class Instant internal constructor(
24+
internal val value: jtInstant
25+
) : Comparable<Instant>, java.io.Serializable {
2426

2527
public actual val epochSeconds: Long
2628
get() = value.epochSecond
@@ -97,6 +99,25 @@ public actual class Instant internal constructor(internal val value: jtInstant)
9799

98100
internal actual val MIN: Instant = Instant(jtInstant.MIN)
99101
internal actual val MAX: Instant = Instant(jtInstant.MAX)
102+
103+
@JvmStatic
104+
private val serialVersionUID: Long = 1L
105+
}
106+
107+
private fun writeObject(oStream: java.io.ObjectOutputStream) {
108+
oStream.defaultWriteObject()
109+
oStream.writeObject(value.toString())
110+
}
111+
112+
private fun readObject(iStream: java.io.ObjectInputStream) {
113+
iStream.defaultReadObject()
114+
val field = this::class.java.getDeclaredField(::value.name)
115+
field.isAccessible = true
116+
field.set(this, jtOffsetDateTime.parse(fixOffsetRepresentation(iStream.readObject() as String)).toInstant())
117+
}
118+
119+
private fun readObjectNoData() {
120+
throw java.io.InvalidObjectException("Stream data required")
100121
}
101122
}
102123

core/jvm/src/LocalDate.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ import java.time.LocalDate as jtLocalDate
1818
import kotlin.internal.*
1919

2020
@Serializable(with = LocalDateIso8601Serializer::class)
21-
public actual class LocalDate internal constructor(internal val value: jtLocalDate) : Comparable<LocalDate> {
21+
public actual class LocalDate internal constructor(
22+
internal val value: jtLocalDate
23+
) : Comparable<LocalDate>, java.io.Serializable {
2224
public actual companion object {
2325
public actual fun parse(input: CharSequence, format: DateTimeFormat<LocalDate>): LocalDate =
2426
if (format === Formats.ISO) {
@@ -50,6 +52,9 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
5052
@Suppress("FunctionName")
5153
public actual fun Format(block: DateTimeFormatBuilder.WithDate.() -> Unit): DateTimeFormat<LocalDate> =
5254
LocalDateFormat.build(block)
55+
56+
@JvmStatic
57+
private val serialVersionUID: Long = 1L
5358
}
5459

5560
public actual object Formats {
@@ -100,6 +105,22 @@ public actual class LocalDate internal constructor(internal val value: jtLocalDa
100105
@PublishedApi
101106
@JvmName("toEpochDays")
102107
internal fun toEpochDaysJvm(): Int = value.toEpochDay().clampToInt()
108+
109+
private fun writeObject(oStream: java.io.ObjectOutputStream) {
110+
oStream.defaultWriteObject()
111+
oStream.writeObject(value.toString())
112+
}
113+
114+
private fun readObject(iStream: java.io.ObjectInputStream) {
115+
iStream.defaultReadObject()
116+
val field = this::class.java.getDeclaredField(::value.name)
117+
field.isAccessible = true
118+
field.set(this, jtLocalDate.parse(iStream.readObject() as String))
119+
}
120+
121+
private fun readObjectNoData() {
122+
throw java.io.InvalidObjectException("Stream data required")
123+
}
103124
}
104125

105126
/**

core/jvm/src/LocalDateTimeJvm.kt

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import java.time.format.DateTimeParseException
1515
import java.time.LocalDateTime as jtLocalDateTime
1616

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

2023
public actual constructor(year: Int, month: Int, day: Int, hour: Int, minute: Int, second: Int, nanosecond: Int) :
2124
this(try {
@@ -104,12 +107,30 @@ public actual class LocalDateTime internal constructor(internal val value: jtLoc
104107
@Suppress("FunctionName")
105108
public actual fun Format(builder: DateTimeFormatBuilder.WithDateTime.() -> Unit): DateTimeFormat<LocalDateTime> =
106109
LocalDateTimeFormat.build(builder)
110+
111+
@JvmStatic
112+
private val serialVersionUID: Long = 1L
107113
}
108114

109115
public actual object Formats {
110116
public actual val ISO: DateTimeFormat<LocalDateTime> = ISO_DATETIME
111117
}
112118

119+
private fun writeObject(oStream: java.io.ObjectOutputStream) {
120+
oStream.defaultWriteObject()
121+
oStream.writeObject(value.toString())
122+
}
123+
124+
private fun readObject(iStream: java.io.ObjectInputStream) {
125+
iStream.defaultReadObject()
126+
val field = this::class.java.getDeclaredField(::value.name)
127+
field.isAccessible = true
128+
field.set(this, jtLocalDateTime.parse(iStream.readObject() as String))
129+
}
130+
131+
private fun readObjectNoData() {
132+
throw java.io.InvalidObjectException("Stream data required")
133+
}
113134
}
114135

115136
/**

core/jvm/src/LocalTimeJvm.kt

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import java.time.format.DateTimeParseException
1616
import java.time.LocalTime as jtLocalTime
1717

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

2224
public actual constructor(hour: Int, minute: Int, second: Int, nanosecond: Int) :
2325
this(
@@ -84,12 +86,31 @@ public actual class LocalTime internal constructor(internal val value: jtLocalTi
8486
@Suppress("FunctionName")
8587
public actual fun Format(builder: DateTimeFormatBuilder.WithTime.() -> Unit): DateTimeFormat<LocalTime> =
8688
LocalTimeFormat.build(builder)
89+
90+
@JvmStatic
91+
private val serialVersionUID: Long = 1L
8792
}
8893

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

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

95116
@Deprecated(

core/jvm/src/UtcOffsetJvm.kt

Lines changed: 19 additions & 1 deletion
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

Lines changed: 50 additions & 0 deletions
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)