Jackson objectmapper adjust timzone to Europe/Amsterdam - kotlin

I'm writing a objectmapper that converts the incomming dates and converts them to my own timezone
fun objectMapperCustomizer(): Jackson2ObjectMapperBuilderCustomizer
{
return Jackson2ObjectMapperBuilderCustomizer { builder ->
builder.timeZone(TimeZone.getDefault())
builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
builder.featuresToEnable(SerializationFeature.WRITE_DATES_WITH_CONTEXT_TIME_ZONE)
builder.serializationInclusion(JsonInclude.Include.NON_NULL)
}
}
But it's not converting to my localtimezone
Expected :"2022-09-12T13:53:10.864+02:00"
Actual :"2022-09-12T13:53:10.864Z"

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.

InvalidFormatException when deserializing String to BigDecimal

Trying to deserialize a String to BigDecimal with a different format. The standard format, e.g. "1,000.20" works. However, in my csv the decimal delimiter and group separator are the other way round. So "1.000,20" would be the number one thousand with 20 as the two decimal places.
data class Record(
#field:JsonProperty("Amount")
val amount: BigDecimal,
)
The mapper is created with
val csvMapper = CsvMapper().apply {
registerModule(KotlinModule.Builder().build())
registerModule(JavaTimeModule())
enable(CsvParser.Feature.TRIM_SPACES)
enable(CsvParser.Feature.SKIP_EMPTY_LINES)
}
The file is read with
InputStreamReader(file.inputStream).use { reader ->
csvMapper.readerFor(Record::class.java)
.with(CsvSchema.emptySchema().withHeader().withColumnSeparator(';'))
.readValues<Record>(reader)
.readAll()
.toList();
Exception:
com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.math.BigDecimal` from String "-1,23": not a valid representation
at [Source: (InputStreamReader); line: 2, column: 53] (through reference chain: org.abc.Record["Amount"])
How can the format be specified?
Just found a question on stackoverflow including the solution to my question. The following works for me
class BigDecimalCustomDeserializer : JsonDeserializer<BigDecimal>() {
val dec = DecimalFormat("###,###,##0.0#", DecimalFormatSymbols(Locale.MYLOCALE))
override fun deserialize(parser: JsonParser, context: DeserializationContext?): BigDecimal? {
return if (parser.text != null && parser.text.isNotEmpty())
dec.parse(parser.text).toString().toBigDecimal()
else null
}
}
with
#field:JsonDeserialize(using = BigDecimalCustomDeserializer::class)
val amount: BigDecimal,

Is it possible to perform objectmapper multiple deserializations of the same type?

I want to change the string type using objectMapper custom deserializer in the following order.
trim string value
append other string
Below is the code I wrote
class ApplicationTests {
#Test
fun test() {
val objectMapper = Jackson2ObjectMapperBuilder.json()
.modules(
SimpleModule().addDeserializer(String::class.java, StringTrimDeserializer()),
SimpleModule().addDeserializer(String::class.java, AppendStringDeserializer("hello"))
)
.build<ObjectMapper>()
val json = "{ \"key\": \"value \" }".trimIndent()
println(objectMapper.readValue(json, Response::class.java))
}
class StringTrimDeserializer : StringDeserializer() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): String {
println("String Trim Deserializer. value=${p.valueAsString}")
return p.valueAsString.trim()
}
}
class AppendStringDeserializer(private val appender: String) : StringDeserializer() {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): String {
println("WorldString Deserializer. value=${p.valueAsString}")
return "${p.valueAsString} $appender"
}
}
data class Response(#JsonProperty("key") val key: String)
}
I expect the result
StringTrimDeserializer. value=value
AppendStringDeserializer. value=value
Response(key=valuehello)
But, the result is
AppendStringDeserializer. value=value
Response(key=value hello)
I expected StringTrimDeserializer to run, but it didn't. Can I run only one thing for the same type of deserializer?

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

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.

Moshi LocalDateTime adapter with multiple format

By default, ThreeTenABP.LocalDateTime is converted to
{"date":{"day":10,"month":4,"year":2018},"time":{"hour":3,"minute":34,"nano":115000000,"second":18}}
I can write an adapter to support ISO date string 2018-04-10T03:45:26.009
class LocalDateTimeAdapter {
#ToJson
fun toJson(value: LocalDateTime): String {
return FORMATTER.format(value)
}
#FromJson
fun fromJson(value: String): LocalDateTime {
return FORMATTER.parse(value, LocalDateTime.FROM)
}
companion object {
private val FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME
}
}
How can I write an adapter which can support both format (fromJson)
{"date":{"day":10,"month":4,"year":2018},"time":{"hour":3,"minute":34,"nano":115000000,"second":18}}
2018-04-10T03:45:26.009
Beside identifying which the format is used in fromJson, I am curious how Moshi internally perform toJson/fromJson for LocalDateTime
You’ll need to use JsonReader.peek() to determine the format of the incoming JSON, and then take action accordingly.
First install an adapter that converts LocalDateTime to a string. That adapter should use a qualifier annotation.
#Retention(RetentionPolicy.RUNTIME)
#JsonQualifier
#interface DateString {
}
Next create the string adapter. It should be straightforward, and might delegate to Moshi’s built-in Rfc3339DateJsonAdapter.
public final class LocalDateAsStringAdapter {
#ToJson String toJson(#DateString LocalDateTime localDateTime) {
...
}
#FromJson #DateString LocalDateTime fromJson(String string) {
...
}
}
Finally create an adapter that delegates either to Moshi’s built in adapter (that one will use {...}) or to your string adapter. This one prefers the string format, but you can do what you like.
public final class MultipleFormatsDateAdapter {
#ToJson void toJson(JsonWriter writer, LocalDateTime value,
#DateString JsonAdapter<LocalDateTime> stringAdapter) throws IOException {
stringAdapter.toJson(writer, value);
}
#FromJson LocalDateTime fromJson(JsonReader reader, #DateString JsonAdapter<LocalDateTime> stringAdapter,
JsonAdapter<LocalDateTime> defaultAdapter) throws IOException {
if (reader.peek() == JsonReader.Token.STRING) {
return stringAdapter.fromJson(reader);
} else {
return defaultAdapter.fromJson(reader);
}
}
}
This works because Moshi lets you declare multiple JsonAdapter arguments to the #ToJson and #FromJson methods, and these arguments may be annotated.
It also relies on the way this feature works if the types are the same. Here we’re making a JsonAdapter<LocalDateTime> by delegating to another JsonAdapter<LocalDateTime>. When the types are the same Moshi uses its nextAdapter() feature for composition.