Support deserialization of inheritance chained objects in kotlin with jackson - kotlin

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

Related

Polymorphic serialization of sealed hierarchies with generic type parameters

Using Kotlin serialization, I would like to serialize and deserialize (to JSON) a generic data class with type parameter from a sealed hierarchy. However, I get a runtime exception.
To reproduce the issue:
import kotlinx.serialization.*
import kotlin.test.Test
import kotlin.test.assertEquals
/// The sealed hierarchy used a generic type parameters:
#Serializable
sealed interface Coded {
val description: String
}
#Serializable
#SerialName("CodeOA")
object CodeOA: Coded {
override val description: String = "Code Object OA"
}
#Serializable
#SerialName("CodeOB")
object CodeOB: Coded {
override val description: String = "Code Object OB"
}
/// Simplified class hierarchy
#Serializable
sealed interface NumberedData {
val number: Int
}
#Serializable
#SerialName("CodedData")
data class CodedData<out C : Coded> (
override val number: Int,
val info: String,
val code: C
): NumberedData
internal class GenericSerializerTest {
#Test
fun `polymorphically serialize and deserialize a CodedData instance`() {
val codedData: NumberedData = CodedData(
number = 42,
info = "Some test",
code = CodeOB
)
val codedDataJson = Json.encodeToString(codedData)
val codedDataDeserialized = Json.decodeFromString<NumberedData>(codedDataJson)
assertEquals(codedData, codedDataDeserialized)
}
}
Running the test results in the following runtime exception:
kotlinx.serialization.SerializationException: Class 'CodeOB' is not registered for polymorphic serialization in the scope of 'Coded'.
Mark the base class as 'sealed' or register the serializer explicitly.
This error message does not make sense to me, as both hierarchies are sealed and marked as #Serializable.
I don't understand the root cause of the problem - do I need to explicitly register one of the plugin-generated serializers? Or do I need to roll my own serializer? Why would that be the case?
I am using Kotlin 1.7.20 with kotlinx.serialization 1.4.1
Disclaimer: I do not consider my solution to be very statisfying, but I cannot find a better way for now.
KotlinX serialization documentation about sealed classes states (emphasis mine):
you must ensure that the compile-time type of the serialized object is a polymorphic one, not a concrete one.
In the following example of the doc, we see that serializing child class instead of parent class prevent it to be deserialized using parent (polymorphic) type.
In your case, you have nested polymorphic types, so this is even more complicated I think. To make serialization and deserialization work, then, I've tried multiple things, and finally, the only way I've found to make it work is to:
Remove generic on CodedData (to be sure that code attribute is interpreted in a polymorphic way:
#Serializable
#SerialName("CodedData")
data class CodedData (
override val number: Int,
val info: String,
val code: Coded
): NumberedData
Cast coded data object to NumberedData when encoding, to ensure polymorphism is triggered:
Json.encodeToString<NumberedData>(codedData)
Tested using a little main program based on your own unit test:
fun main() {
val codedData = CodedData(
number = 42,
info = "Some test",
code = CodeOB
)
val json = Json.encodeToString<NumberedData>(codedData)
println(
"""
ENCODED:
--------
$json
""".trimIndent()
)
val decoded = Json.decodeFromString<NumberedData>(json)
println(
"""
DECODED:
--------
$decoded
""".trimIndent()
)
}
It prints:
ENCODED:
--------
{"type":"CodedData","number":42,"info":"Some test","code":{"type":"CodeOB"}}
DECODED:
--------
CodedData(number=42, info=Some test, code=CodeOB(description = Code Object OB))

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

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>,
//...
)

GSON Deserialization of subtypes in Kotlin

I'm not sure if this is a limitation, a bug or just bad use of GSON. I need to have a hierarchy of Kotlin objects (parent with various subtypes) and I need to deserialize them with GSON. The deserialized object has correct subtype but its field enumField is actually null.
First I thought this is because the field is passed to the "super" constructor but then I found out that "super" works well for string, just enum is broken.
See this example:
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.typeadapters.RuntimeTypeAdapterFactory
open class Parent(val stringField: String,
val enumField: EnumField) {
enum class EnumField {
SUBTYPE1,
SUBTYPE2,
SUBTYPE3
}
}
class Subtype1() : Parent("s1", EnumField.SUBTYPE1)
class Subtype2(stringField: String) : Parent(stringField, EnumField.SUBTYPE2)
class Subtype3(stringField: String, type: EnumField) : Parent(stringField, type)
val subtypeRAF = RuntimeTypeAdapterFactory.of(Parent::class.java, "enumField")
.registerSubtype(Subtype1::class.java, Parent.EnumField.SUBTYPE1.name)
.registerSubtype(Subtype2::class.java, Parent.EnumField.SUBTYPE2.name)
.registerSubtype(Subtype3::class.java, Parent.EnumField.SUBTYPE3.name)
fun main() {
val gson = GsonBuilder()
.registerTypeAdapterFactory(subtypeRAF)
.create()
serializeAndDeserialize(gson, Subtype1()) // this works (but not suitable)
serializeAndDeserialize(gson, Subtype2("s2")) // broken
serializeAndDeserialize(gson, Subtype3("s3", Parent.EnumField.SUBTYPE3)) // broken
}
private fun serializeAndDeserialize(gson: Gson, obj: Parent) {
println("-----------------------------------------")
val json = gson.toJson(obj)
println(json)
val obj = gson.fromJson(json, Parent::class.java)
println("stringField=${obj.stringField}, enumField=${obj.enumField}")
}
Any ideas how to achieve to deserialization of enumField?
(deps: com.google.code.gson:gson:2.8.5, org.danilopianini:gson-extras:0.2.1)
P.S.: Note that I have to use RuntimeAdapterFactory because I have subtypes with different set of fields (I did not do it in the example so it is easier to understand).
Gson requires constructors without arguments to work properly (see deep-dive into Gson code below). Gson constructs raw objects and then use reflection to populate fields with values.
So if you just add some argument-less dummy constructors to your classes that miss them, like this:
class Subtype1() : Parent("s1", EnumField.SUBTYPE1)
class Subtype2(stringField: String) : Parent(stringField, EnumField.SUBTYPE2) {
constructor() : this("")
}
class Subtype3(stringField: String, type: EnumField) : Parent(stringField, type) {
constructor() : this("", EnumField.SUBTYPE3)
}
you will get the expected output:
-----------------------------------------
{"stringField":"s1","enumField":"SUBTYPE1"}
stringField=s1, enumField=SUBTYPE1
-----------------------------------------
{"stringField":"s2","enumField":"SUBTYPE2"}
stringField=s2, enumField=SUBTYPE2
-----------------------------------------
{"stringField":"s3","enumField":"SUBTYPE3"}
stringField=s3, enumField=SUBTYPE3
Gson deep-dive
If you want to investigate the internals of Gson, a tip is to add an init { } block to Subtype1 since it works and then set a breakpoint there. After it is hit you can move up the call stack, step through code, set more breakpoints etc, to reveal the details of how Gson constructs objects.
By using this method, you can find the Gson internal class com.google.gson.internal.ConstructorConstructor and its method newDefaultConstructor(Class<? super T>) that has code like this (I have simplified for brevity):
final Constructor<? super T> constructor = rawType.getDeclaredConstructor(); // rawType is e.g. 'class Subtype3'
Object[] args = null;
return (T) constructor.newInstance(args);
i.e. it tries to construct an object via a constructor without arguments. In your case for Subtype2 and Subtype3, the code will result in a caught exception:
} catch (NoSuchMethodException e) { // java.lang.NoSuchMethodException: Subtype3.<init>()
return null; // set breakpoint here to see
}
i.e. your original code fails since Gson can't find constructors without arguments for Subtype2 and Subtype3.
In simple cases, the problem with missing argument-less constructors is worked around with the newUnsafeAllocator(Type, final Class<? super T>)-method in ConstructorConstructor, but with RuntimeTypeAdapterFactory that does not work correctly.
I may be missing something in what you're trying to achieve, but is it necessary to use the RuntimeTypeAdapterFactory? If we take out the line where we register that in the Gson builder, so that it reads
val gson = GsonBuilder()
.create()
Then the output returns the enum we would expect, which looks to be serialising / deserialising correctly. I.e. the output is:
-----------------------------------------
{"stringField":"s1","enumField":"SUBTYPE1"}
stringField=s1, enumField=SUBTYPE1
-----------------------------------------
{"stringField":"s2","enumField":"SUBTYPE2"}
stringField=s2, enumField=SUBTYPE2
-----------------------------------------
{"stringField":"s3","enumField":"SUBTYPE3"}
stringField=s3, enumField=SUBTYPE3
It also may be an idea to implement Serializable in Parent. i.e.
open class Parent(val stringField: String, val enumField: EnumField) : Serializable {
enum class EnumField {
SUBTYPE1,
SUBTYPE2,
SUBTYPE3
}
}
Try adding #SerializedName annotation to each enum.
enum class EnumField {
#SerializedName("subtype1")
SUBTYPE1,
#SerializedName("subtype2")
SUBTYPE2,
#SerializedName("subtype3")
SUBTYPE3
}

Kotlin - Transform value at initialisation

i'm new in kotlin and i want to know if we can transform a content value at initialisation : with this example :
#Document
data class Category(
#Id val id: Id? = null,
val label: String
)
Category is a document (entity for mongodb) and when i'm instanciating this object, i want to transform label property in uppercase. How can i do that to stay idiomatic with the language ? The point is to keep the immutable properties of the val keyword.
val categ = Category(label = "Test")
println(categ.label) // --> TEST
Thanks.
You can encapsulate the "upperCasing" into a factory:
data class Category constructor(val label: String) {
init {
if (label != label.toUpperCase()) {
throw IllegalStateException("Label must be uppercase")
}
}
companion object {
fun createInstance(str: String) = Category(str.toUpperCase())
}
}
The init block ensures, that clients don't create unwanted instances with non-upper labels (which should be documented).
Create an instance like this:
val instance = Category.createInstance("xy")
You might want to make explicit that you do transformations if the parameter is not upper case already by naming the factory accordingly, e.g. withTransformedLabel or simply add some documentation ;-)

is it possible to add a template to the getter/setter of a data class?

for example , I want to change all setters this way:
this.a = StringUtils.trim(a);
If it's a java bean, I can do this by modifying the code generating template of the ide. But Intellij seems not support to atomically add getter/setter for kotlin data class.
Is there a way to do this?
There is not a way to do this as of Kotlin 1.1.
A Kotlin data class, for the most part, is a class "to do nothing but hold data".
I think the closest you can get is to validate your data upon class initialization and make your data class properties read-only values. e.g.:
data class Data(val a: String) {
init {
require(a == a.trim())
}
}
The following won't throw an exception:
val a = Data("ab")
val b = a.copy(a = "abc")
While the following will:
val c = a.copy(a = "abc ")
It looks like if you declare the property as private, you can create your own getter/setters for accessing it. This example works for me.
fun main(args: Array<String>) {
var t = test("foo")
t.setHello("bar")
println(t)
}
data class test(private var hello: String) {
fun setHello(blah: String) {
this.hello = blah
}
}
But you will still have an issue when the property is passed in to the constructor. You will probably need to rethink how you are doing this, either declaring the field private and trimming it in the getter, or not using a data class for this instance.