How can I (de)serialize kotlin delegate properties with jackson.
I have a class like this
class MyClass {
var a: Int = 42
set(value) {
val changed = field != value
field = value
if (changed) notifyListeners()
}
... and a dozen other properties that all follow this pattern ...
}
I wanted to simplify that by using
class MyClass {
var a: Int by NotifyUiOnChange(42)
...
private inner class NotifyUiOnChange<T>(initialValue: T) : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
notifyUiListeners()
}
}
}
but then Jackson will ignore that property.
How can I tell Jackson to serialize and deserialize that property anyway?
And how do I then apply #JsonIgnore annotations (or something comparable)?
You must use outdated version on Jackson (or maybe a version for Java, not Kotlin?). I've checked this using "com.fasterxml.jackson.module:jackson-module-kotlin:2.10.+" (resolved to 2.10.1).
I've declared two classes:
class MyClass {
var a: Int = 42
set(value) {
val changed = field != value
field = value
if (changed) notifyListener(field)
}
private fun notifyListener(field: Any?) {
println("changed: $field")
}
}
class MyDelegatedClass {
var a: Int by NotifyUi(42)
private inner class NotifyUi<T>(initialValue: T) : ObservableProperty<T>(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) {
notifyListener(newValue)
}
}
private fun notifyListener(field: Any?) {
println("changed: $field")
}
}
My main function:
fun main() {
val noDelegate = MyClass()
val delegated = MyDelegatedClass()
val mapper = ObjectMapper().registerKotlinModule()
// Deserialization
val noDelegateValue = mapper.writeValueAsString(noDelegate)
val delegatedValue = mapper.writeValueAsString(delegated)
println("No delegate:\t$noDelegateValue")
println("With delegate\t$delegatedValue")
// Serialization
val noDelegateObject = mapper.readValue<MyClass>("{\"a\":42}".trimIndent())
val delegateObject = mapper.readValue<MyDelegatedClass>("{\"a\":42}".trimIndent())
}
Output:
No delegate: {"a":42}
With delegate {"a":42}
changed: 42
We even can see output on delegate when we use delegate property :) (I believe it's a side-effect that should be consider as bug actually)
So, handling delegates is out of the box feature in jackson (I am not sure since when, but I used lazy delegate with jackson in older project I used to participate and there was no problems with delegates).
How to ignore delegated property?
So, you cannot apply JsonIgnore annotation to delegated field, because you will get This annotation is not applicable to target 'member property with delegate'. But, you can define the scope that annotation should be applied. Example below:
class MyDelegateClass {
#get:JsonIgnore // or set:
val a: Int by NotifyUi(42)
}
Unfortunately, seems that it's kind of broken, because you can use get: or set: and it's not apply to getter or setter only, but for both.
Related
I'm writing a client for a third-party REST API that returns JSON with a variety of alternative values instead of proper null or omitting the property entirely if null. Depending on the entity or even property in question, null could be represented by either null, "", "0" or 0.
It's easy enough to make a custom serializer, e.g. something like this works fine:
#Serializable
data class Task(
val id: String,
#Serializable(with = EmptyStringAsNullSerializer::class)
val parentID: String?
)
object EmptyStringAsNullSerializer : KSerializer<String?> {
private val delegate = String.serializer().nullable
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("EmptyStringAsNull", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: String?) {
when (value) {
null -> encoder.encodeString("")
else -> encoder.encodeString(value)
}
}
override fun deserialize(decoder: Decoder): String {
return delegate.deserialize(decoder) ?: ""
}
}
fun main() {
val json = """
{
"id": "37883993",
"parentID": ""
}
""".trimIndent()
val task = Json.decodeFromString(json)
println(task)
}
But annotating many properties like this is a bit ugly/noisy. And I'd also like to use inline/value classes for strong typing, like this:
#Serializable
data class Task(
val id: ID,
val parentID: ID?
/* .... */
) {
#JvmInline
#Serializable
value class ID(val value: String)
}
This means that in addition to annotating these properties I also need a custom serializer for each of them. I tried some generic/parameters-based solution that can work for all cases like this:
open class BoxedNullAsAlternativeValue<T, V>(
private val delegate: KSerializer<T>,
private val boxedNullValue: T,
private val unboxer: (T) -> V
) : KSerializer<T> {
private val unboxedNullValue by lazy { unboxer.invoke(boxedNullValue) }
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor(this::class.simpleName!!, PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: T) {
when (value) {
null -> delegate.serialize(encoder, boxedNullValue)
else -> delegate.serialize(encoder, value)
}
}
override fun deserialize(decoder: Decoder): T {
#Suppress("UNCHECKED_CAST")
return when (val boxedValue = delegate.deserialize(decoder)) {
boxedNullValue -> null as T
else -> boxedValue
}
}
}
But that doesn't work because #Serializable(with = ...) expects a static class reference as argument, so it can't have parameters or generics. Which means I'd still need a concrete object for each inline/value type:
#Serializable
data class Task(
val id: ID, // <-- missing serializer because custom serializer is of type ID? for parentID
val parentID: ID?
) {
#JvmInline
#Serializable(with = IDSerializer::class)
value class ID(val value: String)
}
internal object IDSerializer : BoxedNullAsAlternativeValue<Task.ID?, String>(
delegate = Task.ID.serializer().nullable, // <--- circular reference
boxedNullValue = Task.ID(""),
unboxer = { it.value }
)
That doesn't work because there is no longer a generic delegate like StringSerializer and using Task.ID.serializer() would mean the delegate would be the custom serializer itself, so a circular reference. It also fails to compile because one usage of the ID value class is nullable and the other not, so I would need nullable + non-nullable variants of the custom serializer and I would need to annotate each property individually again, which is noisy.
I tried writing a JsonTransformingSerializer but those need to be passed at the use site where encoding/decoding happens, which means I'd need to write one for the entire Task class, e.g. Json.decodeFromString(TaskJsonTransformingSerializer, json) and then also for all other entities of the api.
I found this feature request for handling empty strings as null, but it doesn't appear to be implemented and I need it for other values like 0 and "0" too.
Question
Using kotlinx.serialization and if necessary ktor 2, how to deserialize values like "", "0" and 0 as null for inline/values classes, considering that:
Properties of the same (value) type can be nullable and non-nullable in the same class, but I'd like to avoid having to annotate each property individually
I'd like a solution that is as generic as possible, i.e. not needing a concrete serializer for each value class
It needs to work both ways, i.e. deserializing and serializing
I read in the documentation that serializing is done in 2 distinct phases: breaking down a complex object to it's constituent primitives (serializing) --> writing the primitives as JSON or any other format (encoding). Or in reverse: decoding -> deserializing;
Ideally I'd let the compiler generate serializers for each value class, but annotate each of them with a reference to one of three value transformers (one each for "", "0" and 0) that sit in between the two phases, inspects the primitive value and replaces it when necessary.
I've been at this for quite some time, so any suggestions would be much appreciated.
I have my code structure like this:
File 1:
abstract class SomeClass {
abstract fun print()
companion object {
val versions = arrayOf(ClassV1::class, ClassV2::class)
}
}
#Serializable
data class ClassV1(val x: Int) : SomeClass() {
override fun print() {
println("Hello")
}
}
#Serializable
data class ClassV2(val y: String) : SomeClass() {
override fun print() {
println("World")
}
}
File 2:
fun <T : SomeClass> getSomeObject(json: String, kClass: KClass<T>): SomeClass {
return Json.decodeFromString(json)
}
fun printData(version: Int, json: String) {
val someClass: SomeClass = getSomeObject(json, SomeClass.versions[version])
someClass.print()
}
I have a json in printData that is a serialized form of some sub-class of SomeClass. I also have a version which is used to determine which class structure does the json represent. Based on the version, I want to de-serialize my json string to the appropriate sub-class of SomeClass.
Right now the getSomeObject function deserializes the json to SomeClass (which crashes, as expected). I want to know if there is a way I can deserialize it to the provided KClass.
I know I can do this like below:
val someClass = when (version) {
0 -> Json.decodeFromString<ClassV1>(json)
else -> Json.decodeFromString<ClassV2>(json)
}
But I am trying to avoid this since I can have a lot of such versions. Is there a better way possible?
It seems to me that the following is what you are looking for:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "version",
visible = false)
#JsonSubTypes(
JsonSubTypes.Type(value = ClassV1::class, name = "V1"),
JsonSubTypes.Type(value = ClassV2::class, name = "V2"))
abstract class SomeClass {
(...)
}
This basically means that your JSON would be deserialized as ClassV1 or ClassV2 based on the JSON property version:
V1 would mean that ClassV1 is the target class;
V2 would mean that ClassV2 is the target class.
You can find more information about this at the following online resources:
https://fasterxml.github.io/jackson-annotations/javadoc/2.4/com/fasterxml/jackson/annotation/JsonTypeInfo.html
https://fasterxml.github.io/jackson-annotations/javadoc/2.5/com/fasterxml/jackson/annotation/JsonSubTypes.Type.html
https://www.baeldung.com/jackson-annotations#jackson-polymorphic-type-handling-annotations
I'm trying to replace the default Javalin JSON serializer Jackson by Kotlinx.serialization.
The documentation show how to do it with GSON serializer.
Unfortunately kotlinx serializer has a different function signature and I can't figure out how to pass arguments through.
Serialization is OK but deserialization with decodeFromString function require to be passed a type given by the mapping function as targetClass.
I'm stuck here:
val kotlinx = Json { coerceInputValues = true }
JavalinJson.toJsonMapper = object : ToJsonMapper {
override fun map(obj: Any): String = kotlinx.encodeToString(obj)
}
JavalinJson.fromJsonMapper = object : FromJsonMapper {
override fun <T> map(json: String, targetClass: Class<T>): T = kotlinx.decodeFromString(json)
}
But I get: Cannot use 'T' as reified type parameter. Use a class instead.
I also tried:
JavalinJson.fromJsonMapper = object : FromJsonMapper {
override inline fun <reified T> map(json: String, targetClass: Class<T>): T = kotlinx.decodeFromString(json)
}
But I get a warning: Override by an inline function and an error: Override by a function with reified type parameter.
I'm new to kotlin and I'm struggling understanding what's wrong with this override.
Try this one:
JavalinJson.toJsonMapper = object : ToJsonMapper {
override fun map(obj: Any): String {
val serializer = serializer(obj.javaClass)
return kotlinx.encodeToString(serializer, obj)
}
}
JavalinJson.fromJsonMapper = object : FromJsonMapper {
override fun <T> map(json: String, targetClass: Class<T>): T {
#Suppress("UNCHECKED_CAST")
val deserializer = serializer(targetClass) as KSerializer<T>
return kotlinx.decodeFromString(deserializer, json)
}
}
I have created a sealed class for the json field Value under CustomAttribute data class. This field can return String or Array of Strings.
How can we deserialize this sealed class from json?
data class CustomAttribute (
val attributeCode: String,
val value: Value
)
sealed class Value {
class StringArrayValue(val value: List<String>) : Value()
class StringValue(val value: String) : Value()
}
One solution is to use a RuntimeTypeAdapterFactory as per the instructions in this answer
val valueTypeAdapter = RuntimeTypeAdapter.of(Value::class.java)
.registerSubtype(StringArrayValue::class.java)
.registerSubtype(StringValue::class.java)
val gson = GsonBuilder().registerTypeAdapter(valueTypeAdapter).create()
RuntimeTypeAdapter is included in the source code for Gson but not exposed as a Maven artifact.
It is designed to be copy/pasted into your project from here
I created a TypeAdapterFactory implementation specifically to support sealed classes and their subtypes. This works similarly to the RuntimeTypeAdapterFactory (and I used it as a guide to write my class), but will specifically only support sealed types, and will deserialize using object instances of objects with a sealed class supertype (RuntimeTypeAdapterFactory will create a new instance of object types, which breaks equality checks when a single instance is the expectation).
private class SealedTypeAdapterFactory<T : Any> private constructor(
private val baseType: KClass<T>,
private val typeFieldName: String
) : TypeAdapterFactory {
private val subclasses = baseType.sealedSubclasses
private val nameToSubclass = subclasses.associateBy { it.simpleName!! }
init {
if (!baseType.isSealed) throw IllegalArgumentException("$baseType is not a sealed class")
}
override fun <R : Any> create(gson: Gson, type: TypeToken<R>?): TypeAdapter<R>? {
if (type == null || subclasses.isEmpty() || subclasses.none { type.rawType.isAssignableFrom(it.java) }) return null
val elementTypeAdapter = gson.getAdapter(JsonElement::class.java)
val subclassToDelegate: Map<KClass<*>, TypeAdapter<*>> = subclasses.associateWith {
gson.getDelegateAdapter(this, TypeToken.get(it.java))
}
return object : TypeAdapter<R>() {
override fun write(writer: JsonWriter, value: R) {
val srcType = value::class
val label = srcType.simpleName!!
#Suppress("UNCHECKED_CAST") val delegate = subclassToDelegate[srcType] as TypeAdapter<R>
val jsonObject = delegate.toJsonTree(value).asJsonObject
if (jsonObject.has(typeFieldName)) {
throw JsonParseException("cannot serialize $label because it already defines a field named $typeFieldName")
}
val clone = JsonObject()
clone.add(typeFieldName, JsonPrimitive(label))
jsonObject.entrySet().forEach {
clone.add(it.key, it.value)
}
elementTypeAdapter.write(writer, clone)
}
override fun read(reader: JsonReader): R {
val element = elementTypeAdapter.read(reader)
val labelElement = element.asJsonObject.remove(typeFieldName) ?: throw JsonParseException(
"cannot deserialize $baseType because it does not define a field named $typeFieldName"
)
val name = labelElement.asString
val subclass = nameToSubclass[name] ?: throw JsonParseException("cannot find $name subclass of $baseType")
#Suppress("UNCHECKED_CAST")
return (subclass.objectInstance as? R) ?: (subclassToDelegate[subclass]!!.fromJsonTree(element) as R)
}
}
}
companion object {
fun <T : Any> of(clz: KClass<T>) = SealedTypeAdapterFactory(clz, "type")
}
}
Usage:
GsonBuilder().registerTypeAdapter(SealedTypeAdapterFactory.of(Value::class)).create()
I have successfully serialized and de-serialized a sealed class in the past, with a disclaimer of using Jackson, not Gson as my serialization engine.
My sealed class has been defined as:
#JsonTypeInfo(use = JsonTypeInfo.Id.MINIMAL_CLASS, include = JsonTypeInfo.As.PROPERTY, visible = true)
sealed class FlexibleResponseModel
class SnapshotResponse(val collection: List<EntityModel>): FlexibleResponseModel()
class DifferentialResponse(val collection: List<EntityModel>): FlexibleResponseModel()
class EventDrivenResponse(val collection: List<EntityEventModel>): FlexibleResponseModel()
class ErrorResponse(val error: String): FlexibleResponseModel()
With the annotations used, it required no further configuration for the Jackson instance to properly serialize and de-serialize instances of this sealed class granted that both sides of the communication possessed a uniform definition of the sealed class.
While I recognise that JsonTypeInfo is a Jackson-specific annotation, perhaps you might consider switching over from Gson if this feature is a must - or you might be able to find an equivalent configuration for Gson which would also include the class identifier in your serialized data.
I'm trying to implement a parameterized ActiveAnnotation and try to get the supplied annotations.
#Active(ExampleProcessor)
annotation ExampleAnnotation {
val String value
}
class ExampleProcessor extends AbstractClassProcessor {
override doRegisterGlobals(ClassDeclaration annotatedClass, extension RegisterGlobalsContext context)
{
val annotation = ??
annotation.value
}
}
What I did
annotatedClass.annotations.filter(ExampleAnnotation).head.value
which sadly leads to a null pointer when using it like so:
#ExampleAnnotation("Hello!")
class MyClass { }
annotatedClass.annotations consist of elements of AnnotationReference type, not Class<?>, you should look up a type for your class and then use it to filter out annotations, e.g.:
val annotation = annotatedClass.findAnnotation(ExampleAnnotation.findUpstreamType)
val value = annotation.getStringValue('value');