I have a simple data class being returned from a REST endpoint.
data class SummarizedReturn(
val NET_CASH_FLOW: BigDecimal,
val ROI_PERCENTAGE: BigDecimal
)
When it is returned, the object looks like this:
{
summarizedReturn: {
net_CASH_FLOW: -194703.12028723184,
roi_PERCENTAGE: -35,
}
}
This is not what I need. I need all letters to be capitalized. So I added the JsonProperty annotation
data class SummarizedReturn(
#JsonProperty("NET_CASH_FLOW")
val NET_CASH_FLOW: BigDecimal,
#JsonProperty("ROI_PERCENTAGE")
val ROI_PERCENTAGE: BigDecimal,
)
This did not change anything. I still get the result the same as above.
I then changed the property names and kept the annotation
data class SummarizedReturn(
#JsonProperty("NET_CASH_FLOW")
val netCashFlow: BigDecimal,
#JsonProperty("ROI_PERCENTAGE")
val roiPercentage: BigDecimal,
)
and that returned what I wanted.
{
summarizedReturn: {
NET_CASH_FLOW: -194703.12028723184,
ROI_PERCENTAGE: -35,
}
}
Why did the annotation not work on the initial version of the class? How can I keep my property names all capitalized and have the Jackson value to be the same?
There is an issue with interoperability of Java annotations in Kotlin code. You can register Jackson's Kotlin module to get rid of this problems:
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
fun main() {
val mapper = jacksonObjectMapper() // <= shortcut to ObjectMapper().registerKotlinModule()
println(mapper.writeValueAsString(SummarizedReturn(
BigDecimal("-194703.12028723184"),
BigDecimal("-35"))))
}
Output:
{"NET_CASH_FLOW":-194703.12028723184,"ROI_PERCENTAGE":-35}
This will also require you to add com.fasterxml.jackson.module:jackson-module-kotlin to your dependencies.
PS: Alternatively you can solve it by using slightly different target:
#JsonPropery => #get:JsonPropery (or field:#JsonPropery in case this data class will also be used for deserialization)
Related
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 am working on a plugin type system where 3rd parties will register classes that will expose data. I don't know exactly what the data will look like but I will enumerate these plugin instances collect the data and I would like to serialise it. A simplified version.
interface DataProvider {
fun getDataThatIsSerializable() : ???
}
What can i set the return type to so that I know that I will be able to serialise it with kotlinx serialisation. I cannot see any common interface that is injected into the class and given thet kotlin doesn't support type classes its not clear how to achieve what I am trying to do?
I considered something like this:
interface DataProvider {
fun getDataThatIsSerializable() : Pair<Any,KSerializer<*>>
}
but i could not pass this into the Json.encodeAsString functions, the types do not match
Are there any other options I can consider?
kotlinx.serialization doesn't like serializing things unless you can tell it exactly what you're working with.
Would it make sense for the DataProvider to be responsible for serializing its own data? Something like:
interface DataProvider {
fun getDataThatIsSerializable() : Any
fun encodeAsJsonString(data: Any) : String
}
#Serializable
data class Foo(val value: Int)
class FooDataProvider : DataProvider {
override fun getDataThatIsSerializable() : Any {
return Foo(7)
}
override fun encodeAsJsonString(data: Any): String {
return Json.encodeToString(Foo.serializer(), data as Foo)
}
}
I need to inject a local value to a class constructor during deserialization. For example, look at the following class.
#Serializable
class SomeClass(val local: Context, val serialized: String)
I want the field local to be skipped during serialization and substituted with some predefined local value during deserialization.
The reason behind is that I'm going to transfer models through network, but operations on these models rely on a local context which I want to inject.
Because I haven't find any standard ways to achieve it, I've decided to make use of contextual serialization. So I have written the serializer:
class ContextualInjectorSerializer<T>(private val localValue: T) : KSerializer<T> {
override val descriptor = SerialDescriptor("ValueInjection", StructureKind.OBJECT)
override fun deserialize(decoder: Decoder): T {
decoder.beginStructure(descriptor).endStructure(descriptor)
return localValue
}
override fun serialize(encoder: Encoder, value: T) {
encoder.beginStructure(descriptor).endStructure(descriptor)
}
}
And used it this way:
// Context is marked with #Serializable(with = ContextSerializer::class)
val json = Json(JsonConfiguration.Stable, SerializersModule {
contextual(Context::class, ContextualInjectorSerializer(context))
})
// serialize/deserialize
Surprisingly, it works pretty fine on JVM. However, when I compiled it to JS and tested, I got TypeError: Cannot read property 'siteId' of undefined. Here siteId is a field of Context which I try to access.
Is there a standard way to inject local parameters? What's wrong with my trick?
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
}
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)
}