kotlin with fastjson parse object error: default constructor not found - kotlin

I am trying use fastjson parse object in Kotlin code. but exception happened when I use JSON.parseObject, here are detail:
My data class:
import com.alibaba.fastjson.JSONObject
data class StatesMessage #JvmOverloads constructor(val command: String =
"states", var states: States = States()) {
fun toJsonString(): String {
return JSONObject.toJSONString(this)
}
data class States(var x: Double = 0.0, var y: Double = 0.0)
}
Then I try to get object from string:
val state = JSON.parseObject(s, StatesMessage::class.java)
But exception throw from fastjson:
Caused by: com.alibaba.fastjson.JSONException: default constructor not found.
class com.example.demo.StatesMessage
at com.alibaba.fastjson.util.JavaBeanInfo.build(JavaBeanInfo.java:475)
at com.alibaba.fastjson.util.JavaBeanInfo.build(JavaBeanInfo.java:221)
at com.alibaba.fastjson.parser.ParserConfig.createJavaBeanDeserializer(ParserConfig.java:670)
at com.alibaba.fastjson.parser.ParserConfig.getDeserializer(ParserConfig.java:587)
at com.alibaba.fastjson.parser.ParserConfig.getDeserializer(ParserConfig.java:398)
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:665)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:365)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:269)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:488)
at com.example.demo.StartupRunner.run(StartupRunner.kt:25)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:813)
... 5 more
all code refer to https://github.com/forest-yang/koltinjson

I think it's a fastjson (till 1.2.54) bug.
When I change to gson, it's work.
/* it will throw exception
val state = JSON.parseObject(s, StatesMessage::class.java)
*/
val state = Gson().fromJson(s, StatesMessage::class.java)
logger.info(state.states.x)
logger.info(state.states.y)

Related

Javalin 4: How to write properly check in call to Validator.check()?

I am trying to adopt existing code of parameter validation from Javalin 3 to Javalin 4. It uses Javalin's Validator class. Here's the code I've got (rewritten):
val messageId = ctx.pathParamAsClass<String>("messageId")
.check(check = { it.trim().matches(Uuid.REGEX) }, errorMessage = "message id must be a UUID")
.get()
.trim()
And I am getting compile error for the check() call:
e:
/home/ivan/.../SourceFile.kt: (53, 6): None of the following functions can be called with the arguments
supplied:
public final fun check(check: Check<String> /* = (String) -> Boolean */, error: ValidationError<String>):
Validator<String> defined in io.javalin.core.validation.Validator
public final fun check(check: Check<String> /* = (String) -> Boolean */, error: String): Validator<String> defined in
io.javalin.core.validation.Validator
I can't understand why there is an error. I assume I should have matched second overload of check(). How to write it correctly?
Note: I have read Javalin 3 to 4 migration guide, which gives example like this:
ctx.queryParamAsClass<Int>("age")
.check({ it >= 18 }, ValidationError("AGE_TOO_LOW", args = mapOf("minAge" to 18)))
.get()
which I seem to follow, except I give it error message as string, but there's matching overload. So what is wrong here?
The cause was that second parameter of check() is called error, not errorMessage, i.e. correct code is like this:
...
.check(check = { it.trim().matches(Uuid.REGEX) }, error = "...")
...

R2DBC: How to bind data class for sql query without needing all parameters?

I am trying to bind my data class for a sql query but I am getting a error when I am not using all the parameters from my data class. Is there a way to check in the sql query which parameters needs binding and which ones do not or allow to bind parameters which are not used. The error looks as following:
Request error
java.lang.IllegalArgumentException: Identifier 'deleted_at' is not a valid identifier. Should be of the pattern '\$([\d]+)'.
at io.r2dbc.postgresql.ExtendedQueryPostgresqlStatement.getIndex(ExtendedQueryPostgresqlStatement.java:196)
Caused by: java.lang.IllegalArgumentException: Identifier 'deleted_at' is not a valid identifier. Should be of the pattern '\$([\d]+)'.
at io.r2dbc.postgresql.ExtendedQueryPostgresqlStatement.getIndex(ExtendedQueryPostgresqlStatement.java:196)
And this is the code I use:
Repository:
client
.sql(
"""
INSERT INTO app_user_settings (uuid, allows_to_process_transactions, app_user_id) VALUES (:uuid, :allows_to_process_transactions, :app_user_id)
RETURNING *
""".trimIndent()
)
.bind(AppUserSettingsWriteConverter(), appUserSettings)
.map(AppUserSettingsReadConverter()::convert)
.awaitOne()
Custom bind method:
fun <T> DatabaseClient.GenericExecuteSpec.bind(
convertor: Converter<T, OutboundRow>,
value: T
): DatabaseClient.GenericExecuteSpec {
val outboundRow = convertor.convert(value!!)!!
val t = outboundRow.toMap()
var execution = this
t.forEach { (t, u) ->
execution = execution.bind(t.toString(), u)
}
return execution
}
WriteConverter:
class AppUserSettingsWriteConverter : Converter<AppUserSettings, OutboundRow> {
override fun convert(source: AppUserSettings): OutboundRow {
val outboundRow = OutboundRow()
if (source.isSaved()) {
outboundRow[SqlIdentifier.unquoted("id")] = Parameter.from(source.id)
}
outboundRow[SqlIdentifier.unquoted("uuid")] = Parameter.from(source.uuid)
outboundRow[SqlIdentifier.unquoted("allows_to_process_transactions")] = Parameter.from(source.allowsToProcessTransactions)
outboundRow[SqlIdentifier.unquoted("app_user_id")] = Parameter.from(source.appUserId)
outboundRow[SqlIdentifier.unquoted("deleted_at")] = Parameter.fromOrEmpty(source.deletedAt, ZonedDateTime::class.java)
return outboundRow
}
}
I am using now a check if deleted_at is empty and then not bind it but would prefer if there is another way to do it.

How to get the string in Kotlin to readable format?

I don't know how to get the string in readable format in my app. My code is:
val allergyList = recipeItem.allergies
allergyList.joinToString()
var allergyString: String = ""
for (allergy in allergyList) {
allergyList[1]
allergyString += " ${allergy}"
println(allergy.toString())
}
holder.recipeSearchPageAllergies.text = allergyString
When I print this I get the allergy string memory space?
Result for each one is something like this:
Allergy#4e8f238
How do I 'decode' it into something readable for a human? It should say 'nut allergy'.
you have some options. If you have the control over the Allergy source code,
you could override toString method:
class Allergy(val name: String) {
override fun toString(): String = "Allergy[name=${name}]"
}
println(Allergy("flowers"))
// Allergy[name=flowers]
also, you can make a data class of it. Data class has sane toString by default. It also has a few nice perks, like by default equals/hashCode generation, and deconstruction to components, allowing you to use it in destructing:
data class Allergy(val name: String)
println(Allergy("peanuts"))
// Allergy(name=peanuts)
otherwise, if you can't modify the source of the Allregy, you can make up an extension method for that:
class Allergy(val name: String)
fun Allergy.readable() = "Allergy[name=${name}]"
println(Allergy("cats").readable())
// Allergy[name=cats]
in your case, you could also make an extension method for collections of allergies to have the format you need:
fun Collection<Allergy>.readable() = joinToString { "Allergy[name=${it.name}]" }
println(
listOf(Allergy("cats"), Allergy("peanuts"), Allergy("flowers")).readable()
)
// Allergy[name=cats], Allergy[name=peanuts], Allergy[name=flowers]
// in your case:
holder.recipeSearchPageAllergies.text = recipeItem.allergies.readable()
// or just
holder.recipeSearchPageAllergies.text = recipeItem.allergies.joinToString { "Allergy[name=${it.name}]" }
You can make it simplier:
val allergiesStr = recipeItem.allergies.map { allergy ->
// your allergy name from allergy variable
}.joinToString(separator = " ")

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")
}
}

ReactiveMongo : How to write macros handler to Enumeration object?

I use ReactiveMongo 0.10.0, and I have following user case class and gender Enumeration object:
case class User(
_id: Option[BSONObjectID] = None,
name: String,
gender: Option[Gender.Gender] = None)
object Gender extends Enumeration {
type Gender = Value
val MALE = Value("male")
val FEMALE = Value("female")
val BOTH = Value("both")
}
And I declare two implicit macros handler:
implicit val genderHandler = Macros.handler[Gender.Gender]
implicit val userHandler = Macros.handler[User]
but, when I run application, I get following error:
Error:(123, 48) No apply function found for reactive.userservice.Gender.Gender
implicit val genderHandler = Macros.handler[Gender.Gender]
^
Error:(125, 46) Implicit reactive.userservice.Gender.Gender for 'value gender' not found
implicit val userHandler = Macros.handler[User]
^
Anybody know how to write macros handler to Enumeration object?
Thanks in advance!
I stumbled upon your question a few times searching for the same answer. I did it this way:
import myproject.utils.EnumUtils
import play.api.libs.json.{Reads, Writes}
import reactivemongo.bson._
object DBExecutionStatus extends Enumeration {
type DBExecutionStatus = Value
val Error = Value("Error")
val Started = Value("Success")
val Created = Value("Running")
implicit val enumReads: Reads[DBExecutionStatus] = EnumUtils.enumReads(DBExecutionStatus)
implicit def enumWrites: Writes[DBExecutionStatus] = EnumUtils.enumWrites
implicit object BSONEnumHandler extends BSONHandler[BSONString, DBExecutionStatus] {
def read(doc: BSONString) = DBExecutionStatus.Value(doc.value)
def write(stats: DBExecutionStatus) = BSON.write(stats.toString)
}
}
You have to create a read/write pair by hand and populate with your values.
Hope you already solved this issue given the question age :D