korlibs / docs.korge.org

MIT License
1 stars 1 forks source link

DateTime being inline prevents serialization capabilities #45

Open runt9 opened 5 years ago

runt9 commented 5 years ago

It would be incredibly handy to be able to pass around a single Date-style object between Java, JavaScript, etc. As such, it would be useful if DateTime was not inline as that currently does not work with kotlinx.serialization's @Serializable annotation. Do you have another way around this or believe that making DateTime serializable isn't a priority?

soywiz commented 5 years ago

DateTime is inline so you can perform operations without generating instances. Also making it non inline would break binary compatibility which won't happen before 2.0. What I could do is to generate another artifact variant like klock-noinline or something like that. But I guess kotlinx.serialization should be updated to support inline classes by serializing the underlying type so not sure if worth.

About the Date conversion: that makes perfect sense to me and I have made a PR ( https://github.com/korlibs/klock/pull/18 ) with those. Please, check the PR and comment it to see if it fits your needs.

clhols commented 5 years ago

I tried to make an external serialiser for DateTime like this:

@Serializer(forClass = DateTime::class)
object DateTimeSerializer : KSerializer<DateTime> {
    override val descriptor: SerialDescriptor
        get() = StringDescriptor.withName("WithCustomDefault")

    private val dateFormatMillis = DateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
    private val dateFormatSeconds = DateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")

    override fun serialize(output: Encoder, obj: DateTime) {
        output.encodeString(obj.format(dateFormatMillis))
    }

    override fun deserialize(input: Decoder): DateTime {
        val json = input.decodeString()
        return try {
            dateFormatMillis.parse(json).local
        } catch (e: DateException) {
            dateFormatSeconds.parse(json).local
        }
    }
} 

But the compiler threw this exception:

exception: java.lang.IllegalStateException: Class DateTime is not externally serializable
    at org.jetbrains.kotlinx.serialization.compiler.backend.common.SerializerCodegen.generate(SerializerCodegen.kt:42)

Which is explained here: https://github.com/Kotlin/kotlinx.serialization/issues/129

So I ended up wrapping DateTime in Date class to avoid the constructor parameter. It looks like this:

class Date {
    internal var dateTime = DateTime(0)

    var time: Long
        get() = dateTime.unixMillisLong
        set(value) {
            dateTime = DateTime(value)
        }

    override fun toString(): String = dateTime.toString()
}

@Serializer(forClass = Date::class)
object DateSerializer : KSerializer<Date> {
    override val descriptor: SerialDescriptor
        get() = StringDescriptor.withName("WithCustomDefault")

    private val dateFormatMillis = DateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
    private val dateFormatSeconds = DateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")

    override fun serialize(output: Encoder, obj: Date) {
        output.encodeString(obj.dateTime.format(dateFormatMillis))
    }

    override fun deserialize(input: Decoder): Date {
        val json = input.decodeString()
        return try {
            Date().apply { dateTime = dateFormatMillis.parse(json).local }
        } catch (e: DateException) {
            Date().apply { dateTime = dateFormatSeconds.parse(json).local }
        }
    }
}

And it works. So now I can use the Date class in my API.

adrian5632 commented 5 years ago

Hi! The noinline artifact would be great! At the moment I cannot use Jackson either. The best I came up with so far is:


import com.fasterxml.jackson.annotation.JsonAutoDetect
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import com.soywiz.klock.DateTime

// The following two lines are required to work around the inlined class (DateTime::class.java at runtime returns Class<double>
private val dummyDateTime: Any = DateTime.fromUnix(0L)
private val clazz = dummyDateTime::class.java as Class<DateTime>

abstract class DateTimeMixIn @JsonCreator constructor(@JsonProperty(defaultValue = "0.0") val unixMillis: Double)

class JacksonDateTimeSerializer : JsonSerializer<DateTime>() {

    override fun serialize(value: DateTime?, gen: JsonGenerator?, provider: SerializerProvider?) {
        when (value) {
            null -> gen?.writeNull()
            else -> gen?.writeNumber(value.unixMillisLong)
        }
    }
}

class JacksonDateTimeDeserializer : StdDeserializer<DateTime>(clazz) {
    override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): DateTime? {
        if (p.currentToken() == JsonToken.VALUE_NULL) {
            return null
        }

        val unix = p.longValue
        return DateTime.fromUnix(unix).also { println("ajhgjhg $this") }
    }
}

fun ObjectMapper.registerKlockDateTimeModule(): ObjectMapper {
    setVisibility(serializationConfig
            .defaultVisibilityChecker
            .withFieldVisibility(JsonAutoDetect.Visibility.ANY)
            .withGetterVisibility(JsonAutoDetect.Visibility.NONE)
            .withSetterVisibility(JsonAutoDetect.Visibility.NONE)
            .withCreatorVisibility(JsonAutoDetect.Visibility.NONE))

    val module = SimpleModule().apply {

        addSerializer(clazz, JacksonDateTimeSerializer())
        addDeserializer(clazz, JacksonDateTimeDeserializer())
    }

    addMixIn(clazz, DateTimeMixIn::class.java)
    addMixIn(Double::class.java, DateTimeMixIn::class.java)

    return registerModule(module)
}

data class X(/*@field:JsonDeserialize(using = JacksonDateTimeDeserializer::class)*/ var d: DateTime?)

fun main(args: Array<String>) {
    val m = ObjectMapper().registerKotlinModule().registerKlockDateTimeModule()
    println(m.writer().writeValueAsString(DateTime.now()))
    val json = "{\"d\": ${DateTime.now().unixMillisLong}}"
    val x = m.readValue(json, X::class.java)
    println(x)
}

I thought mix-ins would help but that does not seem to work at all. If X's constructor has var d: DateTime? = null - it works fine. However when it has no value or is non-nullable, I get the following exception:

Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `pl.sskrail.vodrail.client.klock.X`, problem: java.lang.Double cannot be cast to com.soywiz.klock.DateTime
 at [Source: (String)"{"d": 1546684282600}"; line: 1, column: 2]
    at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
    at com.fasterxml.jackson.databind.DeserializationContext.instantiationException(DeserializationContext.java:1608)
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.wrapAsJsonMappingException(StdValueInstantiator.java:484)
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.rewrapCtorProblem(StdValueInstantiator.java:503)
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createUsingDefault(StdValueInstantiator.java:272)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:277)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:151)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3004)
    at pl.sskrail.vodrail.client.klock.JacksonDateTimeKt.main(JacksonDateTime.kt:104)
Caused by: java.lang.ClassCastException: java.lang.Double cannot be cast to com.soywiz.klock.DateTime
    at pl.sskrail.vodrail.client.klock.X.<init>(JacksonDateTime.kt:79)
    at pl.sskrail.vodrail.client.klock.X.<init>(JacksonDateTime.kt)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at com.fasterxml.jackson.databind.introspect.AnnotatedConstructor.call(AnnotatedConstructor.java:119)
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createUsingDefault(StdValueInstantiator.java:270)
    ... 5 more

So, not sure if having noinline artifact would help or perhaps if DateTime had a default value (0.0 or current time like Java's Date). Any thoughts on that?

// EDIT: I forked the repo and made DateTime data class DateTime(val unixMillis: Double = 0.0). Now Jackson de/serialisation works fine. There's definitely a need for a global solution since many apps exchange data that contain dates.

eliekarouz commented 4 years ago

Hello @soywiz Currently using Klock on Android and iOS. Not sure if this is still valid but any chance we can get an artifact where inline classes feature is disabled? The workarounds are a bit ugly...

soywiz commented 4 years ago

@eliekarouz Check the wrapped package: https://github.com/korlibs/klock/tree/master/klock/src/commonMain/kotlin/com/soywiz/klock/wrapped all the classes are there with the same functionality without the inline

eliekarouz commented 4 years ago

@soywiz Thanks!

dianakarenms commented 3 years ago

This is closed but I found this solution useful while using java.sql.Date with Room db for Android Kotlin

@ExperimentalSerializationApi
@Serializable
@Entity(tableName = "my_entity")
class MyEntity(
        @PrimaryKey(autoGenerate = true) val id: Int,
        @Serializable(with = DateSerializer::class) val created: Date)

@ExperimentalSerializationApi
@Serializer(forClass = Date::class)
object DateSerializer : KSerializer<Date> {
    override val descriptor: SerialDescriptor
        get() = PrimitiveSerialDescriptor("DateSerializer", PrimitiveKind.LONG)

    override fun serialize(encoder: Encoder, value: Date) {
        encoder.encodeLong(value.time)
    }

    override fun deserialize(decoder: Decoder): Date {
        return Date(decoder.decodeLong())
    }
}
soywiz commented 3 years ago

@dianakarenms Thanks! Would you mind if we provide your solution in the documentation?

eliekarouz commented 3 years ago

@dianakarenms @soywiz It was a long time ago, I think I tried this solution before and it does not work. Maybe it is working now because you are using a newer version of Kotlinx serialization 1.1.0+ (Kotlin 1.4.30).

soywiz commented 3 years ago

Yeah. Kotlinx.serialization started supporting inline classes not too long ago and this was not working in the past. Now that it is supported, we can document it and maybe specify the minimum serialization version supported.

dianakarenms commented 3 years ago

@dianakarenms Thanks! Would you mind if we provide your solution in the documentation?

Please go ahead Yes, I got it working with Kotlin 1.4.32 and kotlinx-serialization-json:1.1.0