How to use @kotlinx.serialization.Serializable with java.time.Instant?

Viewed 1523

I'm learning how to use Ktor's HttpClient. And, I'd like it to automatically convert a JSON response into a data class. I think I have all the setup correct (pasted below), but unfortunately, java.time.Instant uses import java.io.Serializable;, which I guess isn't compatible with kotlinx's @kotlinx.serialization.Serializable.

So, how can I get Ktor to recognize Instant as serializable?

val httpClient = HttpClient(CIO) {
    install(JsonFeature) {
        serializer = KotlinxSerializer(Json {
            prettyPrint = true
            isLenient = true
        })
    }
}
val response: MyResponse = httpClient.get(baseUrl() + "/example/path") {
    contentType(ContentType.Application.Json)
}

@kotlinx.serialization.Serializable
data class MyResponse(
    val name: String,
    val time: Instant // ERROR: "Serializer has not been found for type 'Instant'. To use context serializer as fallback, explicitly annotate type or property with @Contextual"
)

Sidenote: Other answers using Gson or Jackson or other serializers could be useful too since they may not have to explicitly add @Serializable

2 Answers

Another solution, if you want to keep using java.time, is to create your own serializer for java.time.Instant (note experiemental APIs are used):

import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import java.time.Instant

@Serializer(forClass = Instant::class)
@OptIn(ExperimentalSerializationApi::class)
object InstantSerializer : KSerializer<Instant> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Instant", PrimitiveKind.LONG)
    override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeLong(value.toEpochMilli())
    override fun deserialize(decoder: Decoder) = Instant.ofEpochMilli(decoder.decodeLong())
}

Then you'd write the serializable class like this:

import kotlinx.serialization.*
import java.time.Instant

@Serializable
data class Event(
    val name: String, 
    @Serializable(with=InstantSerializer::class) val instant: Instant
)

Note: There are other approaches (e.g., @UseSerializers).

With this setup, the following code:

import kotlinx.serialization.json.*
import java.time.Instant

fun main() {
    val event = Event("Test Event", Instant.now())
    val json = Json.encodeToString(event)
    println("Encoded to JSON  : $json")
    println("Decoded from JSON: ${Json.decodeFromString<Event>(json)}")
}

Gives this output:

Encoded to JSON  : {"name":"Test Event","instant":1648329502803}
Decoded from JSON: Event(name=Test Event, instant=2022-03-26T21:18:22.803Z)
Related