Register deserializer for a wrapped List of type - jackson

I have JSON that is like this:
{
"apps": [
{
"id": "1",
...
},
{
"id": "2",
...
}
]
}
And for example say the Application class looks like this
data class Application(
val id: String
)
I want to deserialize the JSON into a List<Application>, where each {...} is an Application. I was hoping to do this without having to create a wrapper class like Applications, annotating it with #JsonRootName, and then enabling DeserializationFeature.UNWRAP_ROOT_VALUE. The end goal is to have a Retrofit interface that has something like:
#GET("api/apps")
fun listApplications(): Call<List<Application>>
I tried to implement a simple JsonDeserializer (could probably be optimized):
class ApplicationListDeserializer
: JsonDeserializer<List<Application>>() {
companion object {
private val COLLECTION_TYPE: CollectionType = TypeFactory.defaultInstance()
.constructCollectionType(List::class.java, Application::class.java)
}
override fun deserialize(parser: JsonParser, context: DeserializationContext): List<Application> {
val mapper = ObjectMapper()
val node: JsonNode = parser.codec.readTree(parser)
val collectionReader = mapper.readerFor(COLLECTION_TYPE)
return collectionReader.readValue(node.get("apps"))
}
}
I don't see any way to register this deserializer for this specific type. I tried this:
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.KotlinModule
import com.fasterxml.jackson.module.kotlin.readValue
val objectMapper = ObjectMapper().registerModule(KotlinModule())
val module = SimpleModule()
module.addDeserializer(List::class.java, ApplicationListDeserializer())
objectMapper.registerModule(module)
val applications: List<Application> = objectMapper.readValue("""{
"apps": [
{
"id": "/api-catalog-backend"
}
]
}""")
But that fails with:
Can not construct instance of Application: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
at [Source: N/A; line: -1, column: -1] (through reference chain: java.util.ArrayList[0])" type="com.fasterxml.jackson.databind.JsonMappingException"><![CDATA[com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of Application: no suitable constructor found, can not deserialize from Object value (missing default constructor or creator, or perhaps need to add/enable type information?)
at [Source: N/A; line: -1, column: -1] (through reference chain: java.util.ArrayList[0])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:270)
at com.fasterxml.jackson.databind.DeserializationContext.instantiationException(DeserializationContext.java:1456)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1012)
I am using the following dependency versions:
compile("com.fasterxml.jackson.core", "jackson-core", "2.8.6")
compile("com.fasterxml.jackson.module", "jackson-module-kotlin", "2.8.4")
compile(kotlinModule("stdlib", "1.1-M03"))
compile(kotlinModule("reflect", "1.1-M03"))
How do I configure the deserialization to work correctly with a List<Application>?

Related

How do we use data classes in kotlin to map to AWS Lambda?

I have a simple data class as below:
data class AwsProxyRequest(var key1: String? = null, var key2: String? = null)
I am using API GATEWAY + Lambda proxy integration which invokes a lambda function as below:
class Handler: RequestHandler<AwsProxyRequest, AwsProxyResponse> {
override fun handleRequest(request: AwsProxyRequest, context: Context): AwsProxyResponse {
print(request.toString())
return AwsProxyResponse(body = "parsing error", statusCode = 500)
}
}
I am sending the following body:
{
"key1": "A",
"key2": "B"
}
When I try to invoke the end point, I recieve the following error from lambda:
Caused by: java.io.UncheckedIOException: com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class com.disqo.metering.behavior.api.collector.AwsProxyRequest]: can not instantiate from JSON object (missing default constructor or creator, or perhaps need to add/enable type information?)
It seems that jackson is not able to serialize kotlin data class. Any ideas how we can achieve this using data classes ?

How to validate json in kotlin

There is a kotlin class with the following structure.
data class Person(
#field:Length(max = 5)
val name: String,
val phones: List<Phone>
)
data class Phone(
#field:Length(max = 10)
val number: String
)
When converting the json string through objectMapper, I want to receive all the violation values.
ex) JSON object is not valid. Reasons (3) name length must be 5, number length must be 10, ...
#Test
fun test() {
val json = """
{
"name": "name",
"phones": [
{ "number": "1234567890123456" },
{ "number": "1234567890123456" }
]
}
""".trimIndent()
try {
objectMapper.readValue(json, Person::class.java)
} catch (ex: ConstraintViolationException) {
val violations = ex.constraintViolations
println(violations.size) // expected size = 3
}
}
However, the above code fails to catch the exception and causes the exception below.
com.fasterxml.jackson.databind.JsonMappingException: JSON object is not valid. Reasons (1): {"bean":"Phone","property":"number","value":"1234567890123456","message": "..."}, (through reference chain: Person["phones"]->java.util.ArrayList[0])
Looking at the reason, those wrapped in a list do not throw ConstructionViolationException, but throw JsonMappingException.
below dependencies
plugins {
id("org.springframework.boot") version "2.4.3"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.4.30"
kotlin("plugin.spring") version "1.4.30"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
class BeanValidationDeserializer(base: BeanDeserializerBase?) : BeanDeserializer(base) {
private val logger = LoggerFactory.getLogger(this::class.java)
private val validator = Validation.buildDefaultValidatorFactory().validator
override fun deserialize(parser: JsonParser?, ctxt: DeserializationContext?): Any {
val instance = super.deserialize(parser, ctxt)
validate(instance)
return instance
}
private fun validate(instance: Any) {
val violations = validator.validate(instance)
if (violations.isNotEmpty()) {
val message = StringBuilder()
message.append("JSON object is not valid. Reasons (").append(violations.size).append("): ")
for (violation in violations) {
message.append("{\"bean\":\"${violation.rootBeanClass.name}\",")
.append("\"property\":\"${violation.propertyPath}\",")
.append("\"value\":\"${violation.invalidValue}\",")
.append("\"message\": \"${violation.message}\"}")
.append(", ")
}
logger.warn(message.toString())
throw ConstraintViolationException(message.toString(), violations)
}
}
}
#Bean
fun objectMapper(): ObjectMapper = Jackson2ObjectMapperBuilder.json()
.featuresToDisable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.modules(ParameterNamesModule(), JavaTimeModule(), Jdk8Module(), KotlinModule(), customValidationModule())
.build()
#Bean
fun customValidationModule(): SimpleModule {
val validationModule = SimpleModule()
validationModule.setDeserializerModifier(object : BeanDeserializerModifier() {
override fun modifyDeserializer(
config: DeserializationConfig?,
beanDesc: BeanDescription?,
deserializer: JsonDeserializer<*>?
): JsonDeserializer<*>? {
return if (deserializer is BeanDeserializer) {
BeanValidationDeserializer(deserializer as BeanDeserializer?)
} else deserializer
}
})
return validationModule
}
I'm not sure how to do it. I ask for your help.
I would say an easier and more maintainable way would be to define a JSON Schema.
After that is in place, you can use one of the two json validation libraries mentioned here (https://json-schema.org/implementations.html#validator-kotlin) to validate your json.
The answer by #rbs is good but requires the overhead of creating json schema per json you want to validate.
Seems like object mapper can be configured not to wrap the exceptions it throws with JsonMappingException. - https://github.com/FasterXML/jackson-databind/issues/2033
All you need to do is to disable WRAP_EXCEPTIONS feature for the objectmapper
Note you cant choose a specific exception type, it will not wrap ALL the exceptions.

Deserializing non-null type by passing a default value in Kotlin

I want to deserialize a non-null field in a request model using custom deserializer in Kotlin like this:
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
data class MyRequest(val foo: Foo) {
data class Foo(val bar: String)
companion object {
object Deserializer : StdDeserializer<Foo>(Foo::class.java) { //This is added to Jackson Module successfully somewhere else
override fun deserialize(jsonParser: JsonParser?, context: DeserializationContext?): Foo {
val node: JsonNode = jsonParser!!.codec.readTree(jsonParser)
return if (node.isNull || node.isTextual.not()) Foo("default")
else Foo(node.asText())
}
}
}
}
But when I send a post request with empty json body, I get this:
[org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Instantiation of [simple type, class com.me.myapi.model.request.MyRequest] value failed for JSON property foo due to missing (therefore NULL) value for creator parameter foo which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
Since Foo is a non-null type and I did not pass anything in request body for foo, this is being thrown before deserialization. I wonder if there is a way to handle this exception, such as giving a default value and continue with deserialization step.
With version 2.10.0 of jackson-databind you can have:
data class MyDataClass (
#JsonSetter(nulls = Nulls.SKIP)
val defaultParameter:String="some default value",
)
Also, with version 2.8.4 or above of jackson-kotlin-module you can do:
val mapper: ObjectMapper = ObjectMapper().registerModule(KotlinModule()) // "inform" Jackson about Kotlin
...
data class MyDataClass(
val defaultParameter:String="some default value",
)
I have achieved this simply by overriding getNullValue() method of the deserializer:
object Deserializer : StdDeserializer<Foo>(Foo::class.java) {
override fun deserialize(jsonParser: JsonParser?, context: DeserializationContext?): Foo {
val node: JsonNode = jsonParser!!.codec.readTree(jsonParser)
return if (node.isNull || node.isTextual.not()) Foo("default")
else Foo(node.asText())
}
override fun getNullValue(): Foo {
return Foo("default value")
}
}

Jackson cannot deserialize some non-empty fields

I have a problem with the object deserialization.
My DTO contains a list of Pairs (the previous version was Map).
data class MyDto(
#JsonIgnoreProperties(ignoreUnknown = true)
val myField: List<Pair<String, Boolean>>?
)
And I constantly receive a MissingKotlinParameterException
com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class kotlin.Pair<java.lang.String,java.lang.Boolean>] value failed for JSON property first due to missing (therefore NULL) value for creator parameter first which is a non-nullable type\n at [Source: (io.netty.buffer.ByteBufInputStream); line: 9, column: 33] (through reference chain: my.path.MyDto[\"field\"]->java.util.ArrayList[0]->kotlin.Pair[\"first\"])\n\tat org.springframework.http.codec.json.AbstractJackson2Decoder.processException(AbstractJackson2Decoder.java:162)
my json looks like:
{
"myField" : [
"A": true,
"B": false
]
}
As you can see I have already made the list nullable and put an annotation#JsonIgnoreProperties. But still I get the error.
My configuration for the objectMapper
#Bean
#Primary
fun objectMapper(): ObjectMapper = jacksonObjectMapper().apply {
findAndRegisterModules()
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
.disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
.disable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.disable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT)
.disable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.enable(DeserializationFeature.ACCEPT_FLOAT_AS_INT)
}
What should I also enable/disable to make it work?
Try replacing
List<Pair<String, Boolean>>
with
Map<String, Boolean>
And change the JSON to:
{
"myField" : {
"A": true,
"B": false
}
}
Should work as intended now.

GSON throwing Expected BEGIN_OBJECT but was BEGIN_ARRAY

Facing this problem past few days.Does any guys check this issue? Any help would be greatly appreciated.How can I solved this problem?
GSON throwing Expected BEGIN_OBJECT but was BEGIN_ARRAY
Problem coming from
override fun onSuccess(str_SUCCESS: String)
{
System.out.println("JSON_IS"+str_SUCCESS)
val paymentScheduleModel = Gson().fromJson<PaymentScheduleModel>(str_SUCCESS, PaymentScheduleModel::class.java) // Problem here
}
Json Response is
{
"status": {
"statusCode": 10016,
"isSuccess": true,
"message": "Success"
},
"data": {
"payback_schedule": [
{
"id": 2,
"paid_amount": "INR NaN",
"paidStatus": "Upcoming Payback",
"paid_status": "P",
"s_date": "05/01/2018 12:31:10",
"e_date": "11/01/2018 12:31:10",
"current_balance": "INR 399",
"payanytime_button_status": "active",
"btnColor": "red",
"btnHexColor": "#D2322D"
},
{
"id": 3,
"paid_amount": "INR NaN",
"paidStatus": "Upcoming Payback",
"paid_status": "P",
"s_date": "12/01/2018 12:31:10",
"e_date": "18/01/2018 12:31:10",
"current_balance": "INR 399",
"payanytime_button_status": "active",
"btnColor": "red",
"btnHexColor": "#D2322D"
}
]
}
}
PaymentScheduleModel
data class PaymentScheduleModel(#SerializedName("payback_schedule") val payback_schedule: PaymentSchedule)
data class PaymentSchedule
(#SerializedName("id") val id: Int,
#SerializedName("paid_amount") val paid_amount:String,
#SerializedName("paidStatus") val paidStatus:String,
#SerializedName("paid_status") val paid_status:String,
#SerializedName("s_date") val s_date:String,
#SerializedName("e_date") val e_date:String,
#SerializedName("current_balance") val current_balance:String,
#SerializedName("payanytime_button_status") val payanytime_button_status:String,
#SerializedName("btnColor") val btnColor:String,
#SerializedName("btnHexColor") val btnHexColor:String)
Your model object does not match your Json.
You are trying to parse a JsonObject PaymentScheduleModel which has sub object "payback_schedule" of type PaymentSchedule but you have a JsonObject which has a sub object "data" which is what has the sub object "payback_schedule". So really, you want to parse the "data" block.
You have two options:
1: Create another model that wraps the data block and parse that:
data class PaymentScheduleData(#SerializedName("data") val payback_schedule_model: PaymentScheduleModel)
override fun onSuccess(str_SUCCESS: String) {
val paymentScheduleData = Gson().fromJson<PaymentScheduleData>(str_SUCCESS, PaymentScheduleData::class.java)
// Now use paymentScheduleData.payback_schedule_model
}
2: Pull out the data portion first, then parse:
override fun onSuccess(str_SUCCESS: String) {
// Get the root JsonObject
val jsonObject = Gson().fromJson<JsonObject>(str_SUCCESS, JsonObject::class.java)
// Get the "data" block that matches the model and parse that
val paymentScheduleModel = Gson().fromJson<PaymentScheduleModel>(jsonObject.getAsJsonObject("data"), PaymentScheduleModel::class.java)
}
Hope that helps!
The error is telling you that payback_schedule is holding an array instead of object. So, payback_schedule should be Array<PaymentSchedule> instead of PaymentSchedule.
data class PaymentScheduleModel(#SerializedName("payback_schedule") val payback_schedule: Array<PaymentSchedule>)
PS. You are suggested to implement your own equals() and hashCode() function if your data class contains Array because the default implementation of Array's equals() function compares the referential equality. Suggested reading: Equals method for data class in kotlin