Inconsistent Jackson deserialization of String as LocalDateTime - jackson

I am deserializing a nested collection in JOOQ via Jackson.
The error I am encountering is like this,
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `java.time.LocalDateTime` (no Creators, like default constructor, exist): no String-argument constructor/factory method to deserialize from String value ('2020-12-18T11:29:03.290833') at [Source: (String)"{"wrapped_ts" : "2020-12-18T11:29:03.290833"}"; line: 1, column: 21] (through reference chain: blah.blah.WrappedTimestamp["wrapped_ts"])
This is inconsistent with behavior earlier in the deserialization process. A MRE would be,
=== Function using JOOQ ===
.select(
TOP.ts,
field(
select(
jsonArrayAgg(
jsonObject(
LOWER.lowerTs
)
)
)
.from(LOWER).join(TOP).on(TOP.ID.eq(LOWER.TOP_ID))
).`as`("lowerLevels"),
field(
select(
jsonObject(
WRAPPED.FIRST,
WRAPPED.SECOND
)
)
.from(WRAPPED)
.where(TOP.ID.eq(WRAPPED.TOP_ID))
).`as`("wrapped")
.from(TOP)
.where(condition)
.fetchInto(TopLevel::class.java)
=== Underlying data classes ===
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class TopLevel(
ts: LocalDateTime?,
lowerLevels: List<LowerLevel> = emptyList(),
wrapped: WrappedTimestamp?
)
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class LowerLevel(
lowerTs: LocalDateTime?,
)
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class)
data class WrappedTimestamp(
first: LocalDateTime?,
second: LocalDateTime?,
) {
constructor() : this(
null
}
There are three different levels of timestamps. ts and lowerTs both deserialize fine, wrappedTs throws the error above. If it's unclear, my Jackson ObjectMapper does have the JavaTimeModule registered.
In the course of debugging I have created new deserialization targets at the level of WrappedTimestamp that function with no issue. I have also commented out the wrapped section of the select in JOOQ and it also works.
What could be accounting for this inconsistent behavior?

Related

Kotlin filter list by predicate

I am trying to filter a list based on a condition that a property inside the list is an enum type. But I get an error on the filter function. Can anyone tell me how to resolve this error and why it is happening?
Type inference failed. The value of the type parameter T should be mentioned in input types (argument types, receiver type or expected type). Try to specify it explicitly.
My code is below:
data class Person(
val name: String,
val ageInDays: Int,
val currentStatus: List<Status>,
)
data class Status(
val name: String,
val activity: Activity
)
enum class Activity {
COOK,
CLEAN,
SLEEP,
}
fun main() {
var build = listOf(
Person("abc", 3655, listOf(
Status("abcProc1", Activity.COOK),
Status("abcProc2", Activity.CLEAN),
Status("abcProc2", Activity.SLEEP),
)
),
Person("ghi", 500, listOf(
Status("ghiProc", Activity.COOK),
Status("ghiProc", Activity.SLEEP),
)
),
Person("def", 1000,listOf(
Status("defProc", Activity.SLEEP)
)
)
)
println(build.filter { it.currentStatus.contains(Activity.CLEAN) })
}
currentStatus is a List<Status>. The only type of object that could be in the list is a Status (or subtype thereof). So it doesn't make sense to call contains on the list with an argument that is not a Status. An Activity is not a subtype of Status.
Assuming you want to filter your list of Status to only include instances of Status for which the activity property is Activity.CLEAN, you would do it like:
build.filter { it.currentStatus.any { status -> status.activity == Activity.CLEAN } }
or slightly less efficient but possibly clearer logic:
build.filter { it.currentStatus.map(Status::activity).contains(Activity.CLEAN) }

Retrieving data from CBOR ByteArray

I am trying to serialize a map into CBOR in Kotlin with the Jackson CBOR Dataformats Library, this works fine if the key is a String , I can retrieve the value of that key easily but when the key in an Int, it returns null to me for every get I do, If I print out the output from values(), it gives me all values from all keys.
Code looks like this :
val mapper = CBORMapper()
val map = HashMap<Any,Any>()
map[123] = intArrayOf(22,67,2)
map[456] = intArrayOf(34,12,1)
val cborData = mapper.writeValueAsBytes(map)
println(cborData.toHex())
val deserialized = mapper.readValue(cborData, HashMap<Any,Any>().javaClass)
println(deserialized.get(123)) // returns null
println(values()) // returns all values
Try to iterate over keys and check the type:
deserialized.keys.iterator().next().javaClass
Above code, in your case should print:
123 - class java.lang.String
456 - class java.lang.String
And:
println(deserialized.get("123"))
prints:
[22, 67, 2]
Take a look on documentation:
Module extends standard Jackson streaming API (JsonFactory,
JsonParser, JsonGenerator), and as such works seamlessly with all the
higher level data abstractions (data binding, tree model, and
pluggable extensions).
You can force type using Kotlin's readValue method:
import com.fasterxml.jackson.module.kotlin.readValue
and use it like this:
val deserialized = mapper.readValue<Map<Int, IntArray>>(cborData)
deserialized.keys.forEach { key -> println("$key - ${key.javaClass}") }
println(Arrays.toString(deserialized[123]))
Above code prints:
456 - int
123 - int
[22, 67, 2]
See also:
How to use jackson to deserialize to Kotlin collections

Entity class do not work in room database

following error appears:
Entity class must be annotated with #Entity - androidx.room.Entityerror: Entities and POJOs must have a usable public constructor. You can have an empty constructor or a constructor whose parameters match the fields (by name and type). - androidx.room.Entityerror: An entity must have at least 1 field annotated with #PrimaryKey - androidx.room.Entityerror: [SQLITE_ERROR] SQL error or missing database (near ")": syntax error) - androidx.room.EntityH:\Apps2021\app\build\tmp\kapt3\stubs\debug\de\tetzisoft\danza\data\DAO.java:17: error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such table: geburtstag)
Entity looks like this
#Entity(tableName = "geburtstag")
data class Bday(
#PrimaryKey(autoGenerate = true)
var id : Int,
#ColumnInfo(name="Name")
var name : String,
#ColumnInfo(name="Birthday")
var birth : String
)
The issue would appear to be that you do not have the Bday class defined in the entities in the class that is annotated with #Database.
e.g. you appear to have :-
#Database(entities = [],version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getDao(): Dao
}
This produces the results you have show e.g.
E:\AndroidStudioApps\SO67560510Geburtstag\app\build\tmp\kapt3\stubs\debug\a\a\so67560510geburtstag\TheDatabase.java:7: error: #Database annotation must specify list of entities
public abstract class TheDatabase extends androidx.room.RoomDatabase {
^error: There is a problem with the query: [SQLITE_ERROR] SQL error or missing database (no such table: geburtstag) - a.a.so67560510geburtstag.Dao.getAll()error: Not sure how to convert a Cursor to this method's return type (java.util.List<a.a.so67560510geburtstag.Bday>). - a.a.so67560510geburtstag.Dao.getAll()error: a.a.so67560510geburtstag.Dao is part of a.a.so67560510geburtstag.TheDatabase but this entity is not in the database. Maybe you forgot to add a.a.so67560510geburtstag.Bday to the entities section of the #Database? - a.a.so67560510geburtstag.Dao.insert(a.a.so67560510geburtstag.Bday)
Whilst :-
#Database(entities = [Bday::class],version = 1)
abstract class TheDatabase: RoomDatabase() {
abstract fun getDao(): Dao
}
compiles successfully.
note the change from entities=[] to entities = [Bday::class]
You should set default values for constructor args. In this case kotlin will generate no arg constructor. Try this
#Entity(tableName = "geburtstag")
data class Bday(
#PrimaryKey(autoGenerate = true)
var id : Int = 0,
#ColumnInfo(name="Name")
var name : String = "",
#ColumnInfo(name="Birthday")
var birth : String = ""
)

Why can't Jackson deserialize this JSON?

I'm using Jackson to parse an ElasticSearch document into following data class
data class ElasticCity(
val id: Long,
val regionId: Long,
val countryIso: String,
val isFeatured: Boolean?
) {
// For now Jackson does not support this for constructor parameters https://github.com/FasterXML/jackson-databind/issues/562
#JsonAnySetter
val names: MutableMap<String, String> = mutableMapOf()
}
However I'm getting following error (formatting mine)
com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException:
Instantiation of [simple type, class net.goout.locations.model.ElasticCity] value failed
for JSON property country_iso due to missing (therefore NULL) value for creator parameter countryIso which is a non-nullable type
at [Source: (byte[])
"{
"name.cs":"Brno",
"countryIso":"CZ",
"regionId":85682423,
"timezone":"Europe/Prague",
"name.de":"BrĂ¼nn",
"name.sk":"Brno",
"id":101748109,
"isFeatured":true,
"name.pl":"Brno",
"name.en":"Brno"
}";
line: 1, column: 186] (through reference chain: net.goout.locations.model.ElasticCity["country_iso"])
Clearly the key countryIso is present in the JSON but for some reason Jackson complains a key country_iso is missing. Why? How can I fix this?
try adding
data class ElasticCity(
val id: Long,
val regionId: Long,
#JsonProperty(value = "countryIso") val countryIso: String,
val isFeatured: Boolean?
)
Jackson mapper implicitly converts non start caps characters _
If you want to fix this at multiple places then take a look at
#JsonNaming(PropertyNamingStrategy..
https://www.programcreek.com/java-api-examples/?api=com.fasterxml.jackson.databind.PropertyNamingStrategy

How to deserialize dates with offset ("2019-01-29+01:00") to `java.time` related classes?

I've refactored some legacy code within Spring Boot (2.1.2) system and migrated from java.util.Date to java.time based classes (jsr310). The system expects the dates in a ISO8601 formated string, whereas some are complete timestamps with time information (e.g. "2019-01-29T15:29:34+01:00") while others are only dates with offset (e.g. "2019-01-29+01:00"). Here is the DTO (as Kotlin data class):
data class Dto(
// ...
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
#JsonProperty("processingTimestamp")
val processingTimestamp: OffsetDateTime,
// ...
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-ddXXX")
#JsonProperty("orderDate")
val orderDate: OffsetDateTime,
// ...
)
While Jackson perfectly deserializes processingTimestamp, it fails with orderDate:
Caused by: java.time.DateTimeException: Unable to obtain OffsetDateTime from TemporalAccessor: {OffsetSeconds=32400},ISO resolved to 2018-10-23 of type java.time.format.Parsed
at java.time.OffsetDateTime.from(OffsetDateTime.java:370) ~[na:1.8.0_152]
at com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer.deserialize(InstantDeserializer.java:207) ~[jackson-datatype-jsr310-2.9.8.jar:2.9.8]
This makes sense to me, since OffsetDateTime cannot find any time information necessary to construct the instant. If I change to val orderDate: LocalDate Jackson can successfully deserialize, but then the offset information is gone (which I need to convert to Instant later).
Question
My current workaround is to use OffsetDateTime, in combination with a custom deserializer (see below). But I'm wondering, if there is a better solution for this?
Also, I'd wish for a more appropriate data type like OffsetDate, but I cannot find it in java.time.
PS
I was asking myself if "2019-01-29+01:00" is a valid for ISO8601. However, since I found that java.time.DateTimeFormatter.ISO_DATE is can correctly parse it and I cannot change the format how the clients send data, I put aside this question.
Workaround
data class Dto(
// ...
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-ddXXX")
#JsonProperty("catchDate")
#JsonDeserialize(using = OffsetDateDeserializer::class)
val orderDate: OffsetDateTime,
// ...
)
class OffsetDateDeserializer(
private val formatter: DateTimeFormatter = DateTimeFormatter.ISO_DATE
) : JSR310DateTimeDeserializerBase<OffsetDateTime>(OffsetDateTime::class.java, formatter) {
override fun deserialize(parser: JsonParser, context: DeserializationContext): OffsetDateTime? {
if (parser.hasToken(JsonToken.VALUE_STRING)) {
val string = parser.text.trim()
if (string.isEmpty()) {
return null
}
val parsed: TemporalAccessor = formatter.parse(string)
val offset = if(parsed.isSupported(ChronoField.OFFSET_SECONDS)) ZoneOffset.from(parsed) else ZoneOffset.UTC
val localDate = LocalDate.from(parsed)
return OffsetDateTime.of(localDate.atStartOfDay(), offset)
}
throw context.wrongTokenException(parser, _valueClass, parser.currentToken, "date with offset must be contained in string")
}
override fun withDateFormat(otherFormatter: DateTimeFormatter?): JsonDeserializer<OffsetDateTime> = OffsetDateDeserializer(formatter)
}
As #JodaStephen explained in the comments, OffsetDate was not included in java.time to have a minimal set of classes. So, OffsetDateTime is the best option.
He also suggested to use DateTimeFormatterBuilder and parseDefaulting to create a DateTimeFormatter instance, to directly create OffsetDateTime from the formatters parsing result (TemporalAccessor). AFAIK, I still need to create a custom deserializer to use the formatter. Here is code, which solved my problem:
class OffsetDateDeserializer: JsonDeserializer<OffsetDateTime>() {
private val formatter = DateTimeFormatterBuilder()
.append(DateTimeFormatter.ISO_DATE)
.parseDefaulting(ChronoField.HOUR_OF_DAY, 0)
.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0)
.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0)
.parseDefaulting(ChronoField.MILLI_OF_SECOND, 0)
.parseDefaulting(ChronoField.OFFSET_SECONDS, 0)
.toFormatter()
override fun deserialize(parser: JsonParser, context: DeserializationContext): OffsetDateTime? {
if (parser.hasToken(JsonToken.VALUE_STRING)) {
val string = parser.text.trim()
if (string.isEmpty()) {
return null
}
try {
return OffsetDateTime.from(formatter.parse(string))
} catch (e: DateTimeException){
throw context.wrongTokenException(parser, OffsetDateTime::class.java, parser.currentToken, "error while parsing date: ${e.message}")
}
}
throw context.wrongTokenException(parser, OffsetDateTime::class.java, parser.currentToken, "date with offset must be contained in string")
}
}