rjaros / kvision

Object oriented web framework for Kotlin/JS
https://kvision.io
MIT License
1.2k stars 67 forks source link

Serializer for LocalDateTime not found #294

Closed drammelt closed 2 years ago

drammelt commented 2 years ago

I am getting this error at run time.

kotlinx.serialization.SerializationException: Serializer for class 'LocalDateTime' is not found.
Mark the class as @Serializable or provide the serializer explicitly.
    at kotlinx.serialization.internal.Platform_commonKt.serializerNotRegistered(Platform.common.kt:91)
    at kotlinx.serialization.ContextualSerializer.serializer(ContextualSerializer.kt:51)
    at kotlinx.serialization.ContextualSerializer.serialize(ContextualSerializer.kt:61)
    at kotlinx.serialization.json.internal.StreamingJsonEncoder.encodeSerializableValue(StreamingJsonEncoder.kt:211)
    at kotlinx.serialization.encoding.Encoder$DefaultImpls.encodeNullableSerializableValue(Encoding.kt:302)
    at kotlinx.serialization.encoding.AbstractEncoder.encodeNullableSerializableValue(AbstractEncoder.kt:18)
    at kotlinx.serialization.encoding.AbstractEncoder.encodeNullableSerializableElement(AbstractEncoder.kt:90)
    at xxx.dto.v2.AddressDTO$$serializer.serialize(AddressDTO.kt:11)
    at xxx.dto.v2.AddressDTO$$serializer.serialize(AddressDTO.kt:11)
    at kotlinx.serialization.json.internal.StreamingJsonEncoder.encodeSerializableValue(StreamingJsonEncoder.kt:211)
    at kotlinx.serialization.json.Json.encodeToString(Json.kt:80)
    at io.ktor.serialization.SerializationConverter.serializeContent(SerializationConverter.kt:141)
    at io.ktor.serialization.SerializationConverter.convertForSend(SerializationConverter.kt:128)
    at io.ktor.features.ContentNegotiation$Feature$install$2.invokeSuspend(ContentNegotiation.kt:192)
    at io.ktor.features.ContentNegotiation$Feature$install$2.invoke(ContentNegotiation.kt)
    at io.ktor.features.ContentNegotiation$Feature$install$2.invoke(ContentNegotiation.kt)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:248)
    at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:116)
    at io.ktor.util.pipeline.SuspendFunctionGun.execute(SuspendFunctionGun.kt:136)
    at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:79)
    at xxx.api.v2.views.AccountDataV2$register$1$1.invokeSuspend(AccountDataV2.kt:75)
    at xxx.api.v2.views.AccountDataV2$register$1$1.invoke(AccountDataV2.kt)
    at xxx.api.v2.views.AccountDataV2$register$1$1.invoke(AccountDataV2.kt)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:248)
    at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:116)
    at io.ktor.features.StatusPages$interceptCall$2.invokeSuspend(StatusPages.kt:102)
    at io.ktor.features.StatusPages$interceptCall$2.invoke(StatusPages.kt)
    at io.ktor.features.StatusPages$interceptCall$2.invoke(StatusPages.kt)
    at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:89)
    at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:264)
    at io.ktor.features.StatusPages.interceptCall(StatusPages.kt:101)
    at io.ktor.features.StatusPages.access$interceptCall(StatusPages.kt:18)
    at io.ktor.features.StatusPages$Feature$install$2.invokeSuspend(StatusPages.kt:142)
    at io.ktor.features.StatusPages$Feature$install$2.invoke(StatusPages.kt)
    at io.ktor.features.StatusPages$Feature$install$2.invoke(StatusPages.kt)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:248)
    at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:116)
    at io.ktor.util.pipeline.SuspendFunctionGun.execute(SuspendFunctionGun.kt:136)
    at io.ktor.util.pipeline.Pipeline.execute(Pipeline.kt:79)
    at io.ktor.routing.Routing.executeResult(Routing.kt:155)
    at io.ktor.routing.Routing.interceptor(Routing.kt:39)
    at io.ktor.routing.Routing$Feature$install$1.invokeSuspend(Routing.kt:107)
    at io.ktor.routing.Routing$Feature$install$1.invoke(Routing.kt)
    at io.ktor.routing.Routing$Feature$install$1.invoke(Routing.kt)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:248)
    at io.ktor.util.pipeline.SuspendFunctionGun.proceed(SuspendFunctionGun.kt:116)
    at io.ktor.features.ContentNegotiation$Feature$install$1.invokeSuspend(ContentNegotiation.kt:145)
    at io.ktor.features.ContentNegotiation$Feature$install$1.invoke(ContentNegotiation.kt)
    at io.ktor.features.ContentNegotiation$Feature$install$1.invoke(ContentNegotiation.kt)
    at io.ktor.util.pipeline.SuspendFunctionGun.loop(SuspendFunctionGun.kt:248)
    at io.ktor.util.pipeline.SuspendFunctionGun.access$loop(SuspendFunctionGun.kt:15)
    at io.ktor.util.pipeline.SuspendFunctionGun$continuation$1.resumeWith(SuspendFunctionGun.kt:93)
    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
    at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
    at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164)
    at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:500)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.ktor.server.netty.EventLoopGroupProxy$Companion.create$lambda-1$lambda-0(NettyApplicationEngine.kt:251)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:831)

The common code DTO is as follows.

@file:UseContextualSerialization(LocalDateTime::class)
package xxx.dto.v2

import io.kvision.types.LocalDateTime
import kotlinx.serialization.UseContextualSerialization
import kotlinx.serialization.Serializable
import xxx.fields.ObjectId
import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport

@ExperimentalJsExport
@JsExport
@Serializable
data class AddressDTO (
    val id: ObjectId? = null,
    val addressLine1: String,
    val addressLine2: String,
    val suburb: String,
    val city: String,
    val region: String,
    val areaCode: String,
    val country: String,
    val googlePlaceId: String,
    var deleted: Boolean = false,
    var createdAt: LocalDateTime? = null,
    var updatedAt: LocalDateTime? = null
)

build.grade.kts commonMain

    sourceSets {
        val commonMain by getting {
            dependencies {
                api("io.kvision:kvision-server-ktor:$kvisionVersion")
            }
            kotlin.srcDir("build/generated-src/common")
        }

The values I am inserting into the DTO are from exposed DAO which are in java.time.LocalDateTime public final val createdAt: LocalDateTime / = java.time.LocalDateTime / public final val updatedAt: LocalDateTime / = java.time.LocalDateTime /

I double checked my code against the examples and can find no real differences.

If I change to kotlinx-datetime to serialize the localdatetime instead of the kvision class I have no issues.

Do I need to explicitly set the Serilizer for the kvision LocalDateTime?

rjaros commented 2 years ago

Generally yes, you need to set the serializer explicitly, because on the jvm kvision types are just aliases to java.time.* types (unlike kotlinx-datetime classes, which are custom classes annotated with @Serializable(with = ...) annotations).

drammelt commented 2 years ago

So do I need to do more than adding the

@file:UseContextualSerialization(LocalDateTime::class)

to the DTO? I cannot see anything else in the examples which compile and run fine for me.

rjaros commented 2 years ago

It looks like you are creating a fullstack app, but you don't use KVision fullstack interfaces (at least within this Ktor route). Am I correct? All the examples are using KVision interfaces, where serializers are internally supported for datetime types.

You should be able to reuse KVision serializers module io.kvision.remote.kvSerializersModule (defined here https://github.com/rjaros/kvision/blob/master/kvision-modules/kvision-common-remote/src/jvmMain/kotlin/io/kvision/remote/KotlinxObjectDeSerializer.kt#L46) within Ktor KotlinxSerializer() configuration (https://ktor.io/docs/json.html#kotlinx).

drammelt commented 2 years ago

Yes you are correct this is an AJAX endpoint for a Tabulator table.

You are a lifesaver thank you!

drammelt commented 2 years ago

EDIT: Fixed solution, previous one had issues on the Frontend.

In case anyone comes across this the solution was...

Tabulator Table Ajax -> Backend API Endpoint

Use the Serializer from Kvision in the Backend side

@Serializer(forClass = LocalDateTime::class)
actual object JsonLocalDateTimeSerializer : KSerializer<LocalDateTime> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("java.time.LocalDateTime")

    override fun deserialize(decoder: Decoder): LocalDateTime {
        @Suppress("MagicNumber")
        return LocalDateTime.parse(
            decoder.decodeString().split("[").first().dropLast(6),
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")
        )
    }

    override fun serialize(encoder: Encoder, value: LocalDateTime) {
        encoder.encodeString(value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")))
    }
}

Duplicate the common DTO on the Backend and annotate with @file:UseSerializers(JsonLocalDateTimeSerializer::class) So it is serialised to the format expected by KVision.

And on the JS annotate the common DTO with @file:UseContextualSerialization(LocalDateTime::class)

Backend returns the Ajax DTO which will match the common code DTO and will be used without issue in the Kvision frontend.