Open runt9 opened 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.
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.
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.
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...
@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
@soywiz Thanks!
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())
}
}
@dianakarenms Thanks! Would you mind if we provide your solution in the documentation?
@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).
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 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
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?