Why #JsonIgnore annotation doesn't work during deserializing data? - kotlin

I have a data https://gist.githubusercontent.com/iva-nova-e-katerina/fc1067e971c71a73a0b525a21b336694/raw/954477261bb5ac2f52cee07a8bc45a2a27de1a8c/data2.json a List with seven CheckResultItem elements.
I trying to parse them this way:
import com.fasterxml.jackson.module.kotlin.readValue
...
val res = restHelper.objectMapper.readValue<List<CheckResultItem>>(text)
which gives me the following error:
com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class com.fmetric.validation.api.Brick] value failed for JSON property upperLevelBricks due to missing (therefore NULL) value for creator parameter upperLevelBricks which is a non-nullable type
at [Source: (StringReader); line: 1, column: 714] (through reference chain: java.util.ArrayList[0]->com.fmetric.validation.api.checking.CheckResultItem["brick"]->com.fmetric.validation.api.Brick["upperLevelBricks"])
at com.fasterxml.jackson.module.kotlin.KotlinValueInstantiator.createFromObjectWith(KotlinValueInstantiator.kt:116)
There is #JsonIgnore annotation in data class :
data class Brick(
val id: UUID?,
val name: String,
val type: BrickType,
val propertyValues: List<ProjectBrickPropertyValue<*>>,
#JsonIgnore
val upperLevelBricks: ArrayList<Brick>,
val downLevelBricks: ArrayList<Brick>,
var drawingDetails: List<BrickDrawingDetails>?
) {
But it seems it doesn't work. Could you explain me what is wrong?
UPD: Also I have tried #JsonIgnoreProperties({"upperLevelBricks"}) class annotation but it doesn't work. My solution was to set a default value
val upperLevelBricks: ArrayList<Brick> = arrayListOf(),
But I think that annotations should work!

Actually, it works, but not the way you think. During deserialization #JsonIgnore ignores the respectful field in JSON, like it wasn't there (but it's doesn't make sense in this case, because it's initially absent in JSON).
In Java, Jackson would've just instantiated class with null value for the absent field (because all object types in Java are nullable, which means they allow the value to be set to null). But in Kotlin, a property should be explicitly marked as nullable (val upperLevelBricks: List<Brick>?) or have a default value (val upperLevelBricks: List<Brick> = emptyList()) so that Jackson could create a class instance in this case.
Note that approach with default value for property won't work (unless you additionally mark it with #JsonIgnore) if this field is present in JSON but explicitly set to null:
{
...
"upperLevelBricks": null,
...
}
Anyway, if you don't want to change the API of your Brick class you may provide a default value for this field only when it's created during Jackson deserialization (and only if it's absent/null in JSON) via custom deserializer:
object EmptyListAsDefault : JsonDeserializer<List<Brick>>() {
override fun deserialize(jsonParser: JsonParser, context: DeserializationContext): List<Brick> =
jsonParser.codec.readValue(
jsonParser,
context.typeFactory.constructCollectionType(List::class.java, Brick::class.java)
)
override fun getNullValue(context: DeserializationContext): List<Brick> = emptyList()
}
data class Brick(
//...
#JsonDeserialize(using = EmptyListAsDefault::class)
val upperLevelBricks: List<Brick>,
//...
)

Related

Kotlin NoArgs Constructor for Data Classes with Value Class Property

Let's say we have a value class Id:
#JvmInline
value class Id private constructor(val value: String) {
companion object {
#JvmStatic
#JsonCreator
fun generate() = Id(randomUUID().toString())
}
}
And a simple data class:
data class TestDataClass(
var id: Id? = null,
var string: String? = null,
)
According to the docs, since all properties have default value, Kotlin compiler should generate a default constructor but it doesn't:
// Fails with NoSuchMethodException
System.out.println(TestDataClass.class.getConstructor());
Now, if you change the Id class to a normal one, it does create the default constructor.
It gets even weirder when you add no-args plugin. The above code still doesn't work, however removing the default value like below gets you the default constructor (presumably by the plugin?):
#NoArgs
data class TestDataClass(
var id: Id,
var string: String? = null,
)
Also if you make the Id class a normal one, everything works as expected!
Any idea what's going on? (It seem like a bug to me, but wanted to check here before reporting it.)
Update: Already filed a Kotlin bug report.

Calculated transient value is null after deserialisation with Gson

I run into an error which I do not understand after deserializing with gson.
My usecase is way more complex, but I created this test below which shows the behaviour I see.
The issue is the calculated transient value (allObjs) that is null after deserialisation. The unit test fails on the last assert.
Apparently the #Transient prevents correct initialisation of allObjs after deserialisation.
Is this expected behaviour? Can I do anything to make the field work in this way?
If not then I have to convert all the transient fields to functions like getThemAll(), which does work.
I hope for any insights.
Regards, Rob
class CoupleDeserializeTest {
data class Couple( val objA: String, val objB: String){
#Transient
val allObjs: List<String> = listOf(objA, objB)
fun getThemAll() = listOf(objA, objB)
}
#Test
fun testDe_SerializeCouple() {
val couple = Couple("my", "text")
// This succeeds
Assert.assertNotNull(couple.allObjs)
val gson = Gson()
val json = gson.toJson(couple)
// This succeeds
Assert.assertEquals("{\"objA\":\"my\",\"objB\":\"text\"}", json)
val coupleDeserialized = gson.fromJson<Couple>(json, Couple::class.java)
// This succeeds
Assert.assertNotNull(coupleDeserialized.getThemAll())
// This fails
Assert.assertNotNull(coupleDeserialized.allObjs)
}
}
If you are marking a field as Transient, it will ignore the field from serialisation or deserialisation, snippet from JvmFlagAnnotations
/**
* Marks the JVM backing field of the annotated property as `transient`, meaning that it is not
* part of the default serialized form of the object.
*/
#Target(FIELD)
#Retention(AnnotationRetention.SOURCE)
#MustBeDocumented
public actual annotation class Transient
EDIT
It will ignore it in initialisation because, the value of objA and objB will be initialised using serialisation meaning objA and objB have not assigned by the time you allObjs gets initialised, to get the values later instead of directly assigning, you can use get()
data class Couple( val objA: String, val objB: String){
#Transient
val allObjs: List<String>
get() = listOf(objA, objB)
fun getThemAll() = listOf(objA, objB)
}

Why #Transient can't be used with val fields?

I wrote data class
data class FileHeader(
val relativePath: String,
val orderNumber: Long,
val bodySize: Int
) : Serializable {
#Transient
var headerSize: Int = 0
get() = relativePath.length + 8
}
It works as i expect.
But why i can't use #Transient with val field?
The error is:
This annotation is not applicable to target member property without backing field or delegate
Are there any reasons why it implemented in this way?
The annotation
Marks the JVM backing field of the annotated property as transient, meaning that it is not part of the default serialized form of the object.
The default serialization works on fields and doesn't care about getter methods. So if there's no backing field, there's nothing to serialize (and nothing to mark as transient in bytecode). The annotation would be useless in this case, so the designers chose to make it an error.
If you don't see why there's no backing field:
A backing field will be generated for a property if it uses the default implementation of at least one of the accessors, or if a custom accessor references it through the field identifier.
With your var, the backing field is needed by the default setter; when you change it to val, it isn't.
Try this
#get:javax.persistence.Transient
val headerSize
get() { ... }

Support deserialization of inheritance chained objects in kotlin with jackson

Assume we need to comply deserialization of such object inheritance structure:
open class Parent(
#JsonProperty("parent_value")
val parentValue: String = "default"
)
class Child(
#JsonProperty("child_value")
val childValue: String) : Parent()
Both parent & child object define own fields and #JsonProperty over it.
Also i have a test to check deserialization:
#Test
fun testDeserializeWithInheritance() {
val map = mapOf("child_value" to "success", "parent_value" to "success")
val jsonResult = objectMapper.writerWithDefaultPrettyPrinter()
.writeValueAsString(map)
println("serialized object: $jsonResult")
val deserialized: JsonConverterModuleTest.Child = objectMapper.readValue(jsonResult)
println("deserialized object: withdraw=${deserialized.childValue} parentValue = ${deserialized.parentValue}, exchangeFrom = ${deserialized.parentValue}")
assertEquals("success", deserialized.childValue)
assertEquals("success", deserialized.parentValue)
}
But a problem is the test fails with error:
serialized object: { "child_value" : "success", "parent_value" :
"success" }
org.junit.ComparisonFailure: parent value not equal:
Expected:success
Actual :default
How to deserialize the child object properly? The main goal is to not duplicate fields nor #JsonProperty annotations in child class.
I have a solution for the issue, but open to accept better one
The issue happens because annotation over constructor field is not applied to field nor getter automatically (kotlin mechanizm). Also Seems that it is not processed on deserialization of a child object.
Jackson supports annotations over field or over getter methods, so an appropriate solutions are either
open class Parent(
#get:JsonProperty("parent_value")
val parentValue: String = "default"
)
or
open class Parent(
#field:JsonProperty("parent_value")
val parentValue: String = "default"
)
With this the test completes

Jackson deserialization - Kotlin data classes - Defaults for missing fields per mapper

Given this data class:
data class MyPojo(val notInJson: Int, val inJson: Int)
Assume I want to implement a function of the form:
fun deserialize(jsonString: String, valueForFieldNotInJson: Int): MyPojo
Where jsonString does not include a field named notInJson. Assume also, that I have no control over MyPojo class definition.
How could I use Jackson library to deserialize MyPojo from jsonString and augment the missing field (notInJson) from valueForFieldNotInJson parameter?
Notes:
Basically, the question is about deserializing a Immutable class, where some fields come from Json and others are supplied at runtime.
Using custom deserializers or builders will not work because missing values are unknow at compile time.
This can be achieved by combining MinInAnnotations and ValueInjection.
Complete solution as follows:
data class MyPojo(val notInJson: Int, val inJson: Int)
class MyPojoMixIn {
#JacksonInject("notInJson") val notInJson: Int = 0
}
fun deserialize(jsonString: String, valueForFieldNotInJson: Int): MyPojo {
val injectables = InjectableValues.Std().addValue("notInJson", valueForFieldNotInJson)
val reader = jacksonObjectMapper()
.addMixIn(MyPojo::class.java, MyPojoMixIn::class.java)
.readerFor(MyPojo::class.java)
.with(injectables)
return reader.readValue(jsonString)
}