This is a follow-up question on Jackson YAML: support for anchors and references:
I had a tough time understanding how Jackson treats references (since they don't exist natively in JSON), so I wrote a test:
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.kotlin.KotlinModule
import org.junit.Test
import org.yaml.snakeyaml.Yaml
import kotlin.test.assertEquals
class YamlRefTest {
data class Item(val name: String)
data class Config(val availableItems: List<Item>,
val selectedItems: List<Item>)
#Test
fun testReferenceSnakeYaml() {
val parser = Yaml()
val test = (parser.load(testYaml)
as Map<String, Map<String, List<Map<String, String>>>>)["Config"]!!
assertEquals("test", test["availableItems"]!![0]["name"])
assertEquals("test", test["selectedItems"]!![0]["name"])
}
#Test
fun testReferenceJackson() {
val mapper = ObjectMapper(YAMLFactory())
.registerModule(KotlinModule())
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true)
val config = mapper.readValue(testYaml, Config::class.java)
assertEquals(1, config.selectedItems.size)
assertEquals("test", config.selectedItems[0].name)
}
companion object {
val testYaml = """
Config:
availableItems:
- &a1
name: test
- &a2
name: test2
selectedItems:
- *a1
"""
}
}
The first test passes, the second doesn't. From my understanding of the spec, if you just use anchors (without the merge syntax), YAML treats references pretty much as copy and paste. Why isn't this possible with Jackson? And if it is, what do I need to do to make this work?
This currently fails with:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `undagen.YamlRefTest$Item` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('a1')
at [Source: (StringReader); line: 9, column: 9] (through reference chain: undagen.YamlRefTest$Config["selectedItems"]->java.util.ArrayList[0])
Note: Answers involving the correct use of #JsonIdentityInfo are helpful (I can't seem to grok how this translates to anchors).
Related
I'm trying to implement the motivational example from this page: https://docs.tornadofx.io/0_subsection/1_why_tornadofx
For this I need a data class Person as defined here:
class Person(id: Int, name: String, birthday: LocalDate) {
val idProperty = SimpleIntegerProperty(id)
var id by idProperty
val nameProperty = SimpleStringProperty(name)
var name by nameProperty
val birthdayProperty = SimpleObjectProperty(birthday)
var birthday by birthdayProperty
val age: Int get() = Period.between(birthday, LocalDate.now()).years
}
To do this it was neccessary to make the following imports:
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import java.time.LocalDate
import java.time.Period
However, if I try to run the example I get the following error:
Kotlin: Property delegate must have a 'getValue(Person, KProperty<*>)' method. None of the following functions is suitable:
public open fun getValue(): Int! defined in javafx.beans.property.SimpleIntegerProperty
I can circumvent this by not using delegate types and setting the properties like this:
val idProperty = SimpleIntegerProperty(id)
var id: Int
get() = idProperty.value
set(value) { idProperty.value = value}
But that seems to defeat the point of using delegates in TornadoFX when this is their motivational example for using it.
Here's what I found on delegate types: https://edvin.gitbooks.io/tornadofx-guide/content/part2/Property_Delegates.html
That doesn't help with getting the shorthand of var id by idProperty to work though.
Can somebody point me in the right direction here?
You need to also import the following:
import tornadofx.getValue
import tornadofx.setValue
Those are extension operator functions defined for various types in JavaFX (e.g., properties, observable values, etc.) so that those types can be used as delegates. But those function aren't defined in those types, thus the need for the additional imports.
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))
I'm trying to deserialize object from string, but get "no String-argument constructor/factory method to deserialize from String value ('test')" exception.
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
class KotlinJacksonTest : FunSpec({
val mapper = jacksonObjectMapper()
test("deserialize as string") {
class Ref #JsonCreator constructor(#JsonValue val name: String)
class Root(val ref: Ref)
val root = mapper.readValue<Root>(""" { "ref": "test"} """)
root.ref.name shouldBe "test"
}
})
What I need is to make jackson serialize my object as if it was a string.
But it constantly fails with the following error:
Cannot construct instance of `KotlinJacksonTest$1$1$Ref` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('test')
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `KotlinJacksonTest$1$1$Ref` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('test')
at [Source: (String)" { "ref": "test"} "; line: 1, column: 11] (through reference chain: KotlinJacksonTest$1$1$Root["ref"])
What I'm doing wrong? I can clearly remember this worked for me in java when I needed to do same but with map instead of string.
Ok, after reading some documentation on JsonCreator annoation turns out that it must be like this:
class Ref #JsonCreator(mode = JsonCreator.Mode.DELEGATING) constructor(#JsonValue val name: String)
The JsonCreator.Mode.DELEGATING makes it work like I wanted.
UPD: There is even a possibility to make it shorter - custom jackson annotation.
#Retention(AnnotationRetention.RUNTIME)
#Target(AnnotationTarget.CONSTRUCTOR)
#JacksonAnnotationsInside
#JsonCreator(mode = JsonCreator.Mode.DELEGATING)
annotation class JsonDelegating
And usage:
class Ref #JsonDelegating constructor(#JsonValue val name: String)
You can avoid having to annotate the constructor of your data class completely by just targeting the #JsonValue annotation on your property to its getter method, like so:
class Ref(#get:JsonValue val name: String)
Notably #field:JsonValue does not work in this case either.
Why your original code, nor the field targeting, do not work as expected, I couldn't tell you.
Why can Kotlin's code directly call the top level function from “kotlin.collections”, without import the package. such as below function listOf:
data class Person1(val name: String, val age: Int)
class DataClassExecutor {
... ...
fun test(arg: String?): String? {
val persons = listOf(
Person1("Lucy", age = 26),
Person1("Lily", age = 29))
... ...
}
}
Please refer this page: https://kotlinlang.org/spec/packages-and-imports.html. It says:
There are some packages which have all their entities implicitly
imported into any Kotlin file, meaning one can access such entity
without explicitly using import directives.
The List includes kotlin.collections.
This is similar to how in Java, java.lang is implicitly imported. In Java one does not need to say java.lang.System.out.println, just System.out.println is enough.
I'm new to Kotlin, and experimenting with spring-data-mongodb. Please see example below (also available here as fully runnable Maven project with in-memory MongoDb: https://github.com/danielsindahl/spring-boot-kotlin-example).
Application.kt
package dsitest
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
#SpringBootApplication
open class Application
fun main(args: Array<String>) {
SpringApplication.run(Application::class.java, *args)
}
User.kt
package dsitest
import org.springframework.data.annotation.Id
import org.springframework.data.annotation.PersistenceConstructor
import org.springframework.data.mongodb.core.mapping.Document
#Document(collection = "user")
data class User #PersistenceConstructor constructor(#Id val id: String? = null, val userName: String)
UserRepository.kt
package dsitest
import org.springframework.data.repository.CrudRepository
interface UserRepository : CrudRepository<User, String>
KotlinIntegrationTest.kt
package dsitest
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner
#RunWith(SpringRunner::class)
#SpringBootTest
class KotlinIntegrationTest constructor () {
#Autowired
lateinit var userRepository : UserRepository;
#Test
fun persistenceTest() {
val user : User = User(userName = "Mary")
val savedUser = userRepository.save(user)
val loadedUser = userRepository.findOne(savedUser.id) // Failing code
println("loadedUser = ${loadedUser}")
}
}
When running the test KotlinIntegrationTest.persistenceTest, I get the following error message when trying to retrieve a User object from MongoDb:
org.springframework.data.mapping.model.MappingException: No property null found on entity class dsitest.User to bind constructor parameter to!
If I modify the User data class so that userName is nullable, everything works.
data class User #PersistenceConstructor constructor(#Id val id: String? = null,
val userName: String? = null)
I would like to understand why this is the case, since I don't want userName to be nullable. Is there some alternative, better way of defining my User class in Kotlin?
Many thanks,
Daniel
Yes, it is a known problem. You should check how the bytecode for your User class looks like. Java sees the constructor with all the parameters present and tries to call it with a null value for the 2nd one.
What you could do is to try adding #JvmOverloads to your constructor - this will force Kotlin compiler to generate all versions of the constructor and so the Spring Data Mongo could pick the correct one (get rid of the #PersistenceConstructor) then.
You could also define 1 constructor with no defaults - only for Java-based frameworks and 2nd one with some defaults your you. Or...
When I write things like you are now, I create simple 'persistence' data classes with no default values whatsoever that are mapped to/from my regular domain objects (a sort of abstraction over database). It may generate some overhead at the start - but keeping your domain model not coupled so tightly with the storage model is usually a good idea anyway.