How to parse time stamp and time zone offset simultaneously with Moshi? - kotlin

A JSON-API-response contains the following properties:
created_at_timestamp: 1565979486,
timezone: "+01:00",
I am using Moshi and ThreeTenBp to parse the time stamps and prepared the following custom adapters:
class ZonedDateTimeAdapter {
#FromJson
fun fromJson(jsonValue: Long?) = jsonValue?.let {
try {
ZonedDateTime.ofInstant(Instant.ofEpochSecond(jsonValue), ZoneOffset.UTC) // <---
} catch (e: DateTimeParseException) {
println(e.message)
null
}
}
}
As you can see the zone offset is hardcoded here.
class ZonedDateTimeJsonAdapter : JsonAdapter<ZonedDateTime>() {
private val delegate = ZonedDateTimeAdapter()
override fun fromJson(reader: JsonReader): ZonedDateTime? {
val jsonValue = reader.nextLong()
return delegate.fromJson(jsonValue)
}
}
...
class ZoneOffsetAdapter {
#FromJson
fun fromJson(jsonValue: String?) = jsonValue?.let {
try {
ZoneOffset.of(jsonValue)
} catch (e: DateTimeException) {
println(e.message)
null
}
}
}
...
class ZoneOffsetJsonAdapter : JsonAdapter<ZoneOffset>() {
private val delegate = ZoneOffsetAdapter()
override fun fromJson(reader: JsonReader): ZoneOffset? {
val jsonValue = reader.nextString()
return delegate.fromJson(jsonValue)
}
}
The adapters are registered with Moshi as follows:
Moshi.Builder()
.add(ZoneOffset::class.java, ZoneOffsetJsonAdapter())
.add(ZonedDateTime::class.java, ZonedDateTimeJsonAdapter())
.build()
Parsing the individual fields (created_at_timestamp, timezone) works fine. I want however get rid of the hardcoded zone offset. How can I configure Moshi to fall back on the timezone property when parsing the created_at_timestamp property.
Related
Advanced JSON parsing techniques using Moshi and Kotlin
The work-in-progress branch of the related project

For the created_at_timestamp field you should use a type that doesn't have a timezone. This is usually Instant. It identifies a moment in time independent of which timezone it is being interpreted in.
Then in your enclosing type you can define a getter method to combine the instant and zone into one value. The ZonedDateTime.ofInstant method can do this.

Related

Android retrofit - date format when passing datetime by URL

I have API method mapping like this
#POST("api/updateStarted/{id}/{started}")
suspend fun updateStarted(
#Path("id") id: Int,
#Path("started") started: Date
) : Response <Int>
I want to use yyyy-MM-dd'T'HH:mm:ss format everywhere. My API adapter looks like this:
val gson = GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
val apiClient: ApiClient = Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create(gson.create()))
.baseUrl(API_BASE_URL)
.client(getHttpClient(API_USERNAME, API_PASSWORD))
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiClient::class.java)
However GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss") cannot affect date format when I pass it thru URL (because that's not JSON) so Retrofit builds URL like this:
http://myserver.com/api/updateFinished/2/Fri%20Jan%2027%2013:48:42%20GMT+01:00%202023
instead of something like this:
http://myserver.com/api/updateFinished/2/2023-01-28T02:03:04.000
How can I fix that? I'm new in Retrofit and I don't fully understand date/time libraries in Java.
You can switch the data type from java.util.Date to java.time.LocalDateTime if you want your desired format using the toString() method of that data type.
Currently, you have Date.toString() producing an undesired result.
If you don't have to use a Date, import java.time.LocalDateTime and just change your fun a little to this:
#POST("api/updateStarted/{id}/{started}")
suspend fun updateStarted(
#Path("id") id: Int,
#Path("started") started: LocalDateTime
) : Response <Int>
GsonConverterFactory supports responseBodyConverter and requestBodyConverter which aren't used to convert URL params. For that, you need a stringConverter which, fortunately is trivial to implement:
class MyToStringConverter : Converter<SomeClass, String> {
override fun convert(value: SomeClass): String {
return formatToMyDesiredUrlParamFormat(value)
}
companion object {
val INSTANCE = MyToStringConverter()
}
}
class MyConverterFactory : Converter.Factory() {
override fun stringConverter(type: Type, annotations: Array<out Annotation>, retrofit: Retrofit): Converter<*, String>? {
return if (type == SomeClass::class.java) {
//extra check to make sure the circumstances are correct:
if (annotations.any { it is retrofit2.http.Query }) {
MyToStringConverter.INSTANCE
} else {
null
}
} else {
null
}
}
}
and then
val apiClient: ApiClient = Retrofit.Builder()
.baseUrl(API_BASE_URL)
.client(getHttpClient(API_USERNAME, API_PASSWORD))
.addConverterFactory(GsonConverterFactory.create())
.addConverterFactory(MyConverterFactory())
//(...)
I've added checking for annotations as example if one would want tighter control on when the converter is used.

Implementing observable properties that can also serialize in Kotlin

I'm trying to build a class where certain values are Observable but also Serializable.
This obviously works and the serialization works, but it's very boilerplate-heavy having to add a setter for every single field and manually having to call change(...) inside each setter:
interface Observable {
fun change(message: String) {
println("changing $message")
}
}
#Serializable
class BlahVO : Observable {
var value2: String = ""
set(value) {
field = value
change("value2")
}
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
}
println(BlahVO().apply { value2 = "test2" })
correctly outputs
changing value2
{"value2":"test2"}
I've tried introducing Delegates:
interface Observable {
fun change(message: String) {
println("changing $message")
}
#Suppress("ClassName")
class default<T>(defaultValue: T) {
private var value: T = defaultValue
operator fun getValue(observable: Observable, property: KProperty<*>): T {
return value
}
operator fun setValue(observable: Observable, property: KProperty<*>, value: T) {
this.value = value
observable.change(property.name)
}
}
}
#Serializable
class BlahVO : Observable {
var value1: String by Observable.default("value1")
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
}
println(BlahVO().apply { value1 = "test1" }) correctly triggers change detection, but it doesn't serialize:
changing value1
{}
If I go from Observable to ReadWriteProperty,
interface Observable {
fun change(message: String) {
println("changing $message")
}
fun <T> look(defaultValue: T): ReadWriteProperty<Observable, T> {
return OP(defaultValue, this)
}
class OP<T>(defaultValue: T, val observable: Observable) : ObservableProperty<T>(defaultValue) {
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
super.setValue(thisRef, property, value)
observable.change("blah!")
}
}
}
#Serializable
class BlahVO : Observable {
var value3: String by this.look("value3")
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
}
the result is the same:
changing blah!
{}
Similarly for Delegates.vetoable
var value4: String by Delegates.vetoable("value4", {
property: KProperty<*>, oldstring: String, newString: String ->
this.change(property.name)
true
})
outputs:
changing value4
{}
Delegates just doesn't seem to work with Kotlin Serialization
What other options are there to observe a property's changes without breaking its serialization that will also work on other platforms (KotlinJS, KotlinJVM, Android, ...)?
Serialization and Deserialization of Kotlin Delegates is not supported by kotlinx.serialization as of now.
There is an open issue #1578 on GitHub regarding this feature.
According to the issue you can create an intermediate data-transfer object, which gets serialized instead of the original object. Also you could write a custom serializer to support the serialization of Kotlin Delegates, which seems to be even more boilerplate, then writing custom getters and setters, as proposed in the question.
Data Transfer Object
By mapping your original object to a simple data transfer object without delegates, you can utilize the default serialization mechanisms.
This also has the nice side effect to cleanse your data model classes from framework specific annotations, such as #Serializable.
class DataModel {
var observedProperty: String by Delegates.observable("initial") { property, before, after ->
println("""Hey, I changed "${property.name}" from "$before" to "$after"!""")
}
fun toJson(): String {
return Json.encodeToString(serializer(), this.toDto())
}
}
fun DataModel.toDto() = DataTransferObject(observedProperty)
#Serializable
class DataTransferObject(val observedProperty: String)
fun main() {
val data = DataModel()
println(data.toJson())
data.observedProperty = "changed"
println(data.toJson())
}
This yields the following result:
{"observedProperty":"initial"}
Hey, I changed "observedProperty" from "initial" to "changed"!
{"observedProperty":"changed"}
Custom data type
If changing the data type is an option, you could write a wrapping class which gets (de)serialized transparently. Something along the lines of the following might work.
#Serializable
class ClassWithMonitoredString(val monitoredProperty: MonitoredString) {
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
}
fun main() {
val monitoredString = obs("obsDefault") { before, after ->
println("""I changed from "$before" to "$after"!""")
}
val data = ClassWithMonitoredString(monitoredString)
println(data.toJson())
data.monitoredProperty.value = "obsChanged"
println(data.toJson())
}
Which yields the following result:
{"monitoredProperty":"obsDefault"}
I changed from "obsDefault" to "obsChanged"!
{"monitoredProperty":"obsChanged"}
You however lose information about which property changed, as you don't have easy access to the field name. Also you have to change your data structures, as mentioned above and might not be desirable or even possible. In addition, this work only for Strings for now, even though one might make it more generic though.
Also, this requires a lot of boilerplate to start with. On the call site however, you just have to wrap the actual value in an call to obs.
I used the following boilerplate to get it to work.
typealias OnChange = (before: String, after: String) -> Unit
#Serializable(with = MonitoredStringSerializer::class)
class MonitoredString(initialValue: String, var onChange: OnChange?) {
var value: String = initialValue
set(value) {
onChange?.invoke(field, value)
field = value
}
}
fun obs(value: String, onChange: OnChange? = null) = MonitoredString(value, onChange)
object MonitoredStringSerializer : KSerializer<MonitoredString> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MonitoredString", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: MonitoredString) {
encoder.encodeString(value.value)
}
override fun deserialize(decoder: Decoder): MonitoredString {
return MonitoredString(decoder.decodeString(), null)
}
}

Jackson skip field serialization - custom configuration

i have custom jackson configuration in kotlin:
#Configuration
class JacksonConfiguration {
#Bean
fun objectMapper(): com.fasterxml.jackson.databind.ObjectMapper = jacksonObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
.apply {
registerModules(customJavaTimeModule(), KotlinModule())
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
}
}
private fun customJavaTimeModule() = JavaTimeModule().apply {
addSerializer(String::class.java, StringSerializerTypeHandler())
}
private class StringSerializerTypeHandler : JsonSerializer<String?>() {
#Throws(IOException::class, JsonProcessingException::class)
override fun serialize(value: String?, jgen: JsonGenerator?, provider: SerializerProvider?) {
var outputValue: String = value.toString()
if (!value.isNullOrEmpty()) {
// logic for changing the string - removing diacritics
jgen?.writeString(outputValue)
} else {
//provider?. // what here?
// jgen?.writeString(value)
// dont serialize the field at all - just skip it
}
}
}
What's important is that i do not want that field to be serialized at all when the field value is null or empty.
I can not get rid of else block - then I will have error:
Can not write a field name, expecting a value
I want to set this logic globally.
Solution 1:
I had to override this method:
override fun isEmpty(provider: SerializerProvider?, value: String?): Boolean {
return value.isNullOrEmpty()
}
Then i can get rid of else block.

Compilation throws `None of the following functions can be called with the arguments supplied`

Trying to implement a custom JSONB binding that maps to an object containing a map. Generated code throws a None of the following functions can be called with the arguments supplied error caused by the following line:
val SOME_FIELD: TableField<SomeRecord, Jsonb?> = createField(DSL.name("meta"), SQLDataType.JSONB.nullable(false).defaultValue(DSL.field("'{}'::jsonb", SQLDataType.JSONB)), this, "", JsonbBinding())
Here's my configuration:
class JsonbBinding : Binding<Any, Jsonb> {
private val mapper = ObjectMapper()
override fun converter(): Converter<Any, Jsonb> {
return object : Converter<Any, Jsonb> {
override fun from(dbObject: Any?): Jsonb {
if (dbObject == null) return Jsonb()
val props = mapper.readValue<MutableMap<String, Any>>(dbObject.toString())
return Jsonb(props)
}
override fun to(userObject: Jsonb?): Any? {
return mapper.writeValueAsString(userObject)
}
override fun fromType(): Class<Any> {
return Any::class.java
}
override fun toType(): Class<Jsonb> {
return Jsonb::class.java
}
}
}
override fun sql(ctx: BindingSQLContext<Jsonb>) {
ctx.render()?.let {
if (it.paramType() == ParamType.INLINED) {
it.visit(
DSL.inline(ctx.convert(converter()).value())
).sql("::jsonb")
} else {
it.sql("?::jsonb")
}
}
}
override fun register(ctx: BindingRegisterContext<Jsonb>) {
ctx.statement().registerOutParameter(ctx.index(), Types.VARCHAR)
}
override fun set(ctx: BindingSetStatementContext<Jsonb>) {
ctx.statement().setString(
ctx.index(),
ctx.convert(converter()).value()?.toString()
)
}
override fun set(ctx: BindingSetSQLOutputContext<Jsonb>) {
throw SQLFeatureNotSupportedException()
}
override fun get(ctx: BindingGetResultSetContext<Jsonb>) {
ctx.convert(converter()).value(ctx.resultSet().getString(ctx.index()))
}
override fun get(ctx: BindingGetStatementContext<Jsonb>) {
ctx.convert(converter()).value(ctx.statement().getString(ctx.index()))
}
override fun get(ctx: BindingGetSQLInputContext<Jsonb>) {
throw SQLFeatureNotSupportedException()
}
}
<forcedType>
<userType>org.example.Jsonb</userType>
<binding>org.example.JsonbBinding</binding>
<includeExpression>.*</includeExpression>
<includeTypes>jsonb</includeTypes>
</forcedType>
Also, it seems like the line causing problems is mapping database data to JOOQ's default JSONB object. Is that what's causing the issue? Is there anything I may want to do about it? Is there some other way of doing mapping database JSONB data to a map by JOOQ?
I think you're confusing the type variables on Binding<T, U> here:
T is the database / JDBC type (in this case org.jooq.JSONB)
U is the user type (in this case Any)
You have to implement the binding the other way round: Binding<JSONB?, Any?>. Since jOOQ already takes care of properly binding the JSONB type to JDBC, you can probably do with your Converter<JSONB?, Any?> implementation alone, and attach that to your generated code instead:
class JsonbConverter : Converter<JSONB?, Any?> { ... }
Also, you don't have to use your own Jsonb type to wrap JSON data here.

Kotlin - when expression over class type

I'm attempting to write an invocation handler that uses a map (supplied at runtime) to implement an interface's getters.
This very crudely works. I know the basic types that may be returned, so I'm OK with having a when expression.
I haven't found a way to avoid using the name of the class as the subject of the when expression; is there a better way?
class DynamicInvocationHandler<T>(private val delegate: Map<String, Any>, clzz: Class<T>) : InvocationHandler {
val introspector = Introspector.getBeanInfo(clzz)
val getters = introspector.propertyDescriptors.map { it.readMethod }
override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any? {
if (method in getters) {
// get the value from the map
val representation = delegate[method.name.substring(3).toLowerCase()]
// TODO need better than name
when (method.returnType.kotlin.simpleName) {
LocalDate::class.simpleName -> {
val result = representation as ArrayList<Int>
return LocalDate.of(result[0], result[1], result[2])
}
// TODO a few other basic types like LocalDateTime
// primitives come as they are
else -> return representation
}
}
return null
}
}
You can use the types instead of the class names in the when statement. After a type is matched, Kotlin smart cast will automatically cast it
Example
val temporal: Any? = LocalDateTime.now()
when (temporal){
is LocalDate -> println("dayOfMonth: ${temporal.dayOfMonth}")
is LocalTime -> println("second: ${temporal.second}")
is LocalDateTime -> println("dayOfMonth: ${temporal.dayOfMonth}, second: ${temporal.second}")
}
when expressions support any type (unlike Java's switch), so you can just use the KClass instance itself:
when (method.returnType.kotlin) {
LocalDate::class -> {
...
}
...
}