I want to use Jackson mixin to provide a default implementation for an abstract type:
#JsonTypeInfo(
use = Id.NAME,
include = As.PROPERTY,
property = "type",
visible = true,
defaultImpl = GenericRequest::class
)
#JsonMixin(Request::class)
class AlexaRequestMixin {
}
data class GenericRequest(
val type: String, val requestId: String, val timestamp: OffsetDateTime
)
Base class that I want to alter with a mixin:
#JsonTypeInfo(
use = Id.NAME,
include = As.PROPERTY,
property = "type",
visible = true
)
#JsonSubTypes({#Type(
value = InstallationError.class,
name = "Alexa.DataStore.PackageManager.InstallationError"
),
// ...
)})
public abstract class Request {
My objectMapper:
However when I try to deserialize a class that is not present as subtype I get:
com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Could not resolve type id 'Foo' as a subtype of 'com.amazon.ask.model.Request': known type ids = [...]
In order to make it work I had to:
Make GenericRequest extend from the abstract Request class.
When an abstract class is from Java and inheriting data class is from Kotlin it causes lots of problems.
a) Data class cannot override same properties from abstract class https://youtrack.jetbrains.com/issue/KT-6653/Kotlin-properties-do-not-override-Java-style-getters-and-setters
b) I had to change include = As.PROPERTY to include = JsonTypeInfo.As.WRAPPER_ARRAY
So I ended up with implementing GenericRequest extending Request in Java...
Related
I'm having trouble with kotlin-serialization in the following use case:
#Serializable
sealed class NetworkAnswer {
#SerialName("answerId")
abstract val id: Int
}
#Serializable
data class NetworkYesNoAnswer(
override val id: Int,
#SerialName("isPositive")
val isPositive: Boolean
) : NetworkAnswer()
When I serialize this:
val json = Json { ignoreUnknownKeys = true; explicitNulls = false }
val result: NetworkYesNoAnswer = json.decodeFromString(NetworkYesNoAnswer.serializer(), """
{
"answerId": 1,
"isPositive": true
}
""".trimIndent()
)
I get the following error
Caused by: kotlinx.serialization.MissingFieldException: Fields [id] are required for type with serial name 'NetworkYesNoAnswer', but they were missing
The only way the serialization works is if I use the same name for both the member and "SerialName", like so:
#Serializable
sealed class NetworkAnswer {
#SerialName("answerId")
abstract val answerId: Int
}
#Serializable
data class NetworkYesNoAnswer(
override val answerId: Int,
#SerialName("isPositive")
val isPositive: Boolean
) : NetworkAnswer()
This kinda defeats the purpose of "SerialName", is there a way to solve that without using the same name?
Declaring a #SerialName on a base class has no effect on member declarations overridden by child classes.
Instead, you can declare #SerialName on the child class instead. There is no need to change the actual name of the field.
#Serializable
data class NetworkYesNoAnswer(
#SerialName("answerId")
override val id: Int,
#SerialName("isPositive")
val isPositive: Boolean
) : NetworkAnswer()
Declaring the #SerialName on the base class and applying it to all children seems NOT to be supported as of now, but is desired by other members of the community as well, e.g. here on GitHub.
OT: Most likely you could use a sealed interface, which was first introduced in Kotlin v1.5.0, instead of a sealed class.
I have a list of clases that implement a specific interface. The ability to construct those clases or not is not static (so it's not possible to use when(className)), and can be configured so I want to be able to create some clases or call some methods based on a hashMap of allowed "constructors". Then if the key identifying a class is in present in the hashmap I can call the corresponding method, otherwise I can safely ignore. Let me illustrate:
Let's say I have an interface like
interface Instanceable {
data class Config(
val bar: Whatever
)
fun getIntance(config: Config): Instanceable
}
Then I have several (let's say 10) classes that implement this interface
class Implementation1() : Instanceable {
companion object {
const val ID = "INSTANCE_1"
}
private lateinit var foo: Whatever
override fun getIntance(config: Config) = Implementation1().also{ this#Implementation1.foo = config.bar }
}
I want to create a hashmap of the methods by the identifiers, so later down the lane I can grab the method from the hashMap by the key ID and just invoke() the value if it's there. Something like:
allowedInstances("INSTANCE_1")?.let{ it.invoke(someConfig) }
In order to do this I tried to create a hashMap of methods like this:
private val allowedInstances = mutableHashMapOf<String, Instanceable.(Instanceable.Config)->Instanceable>()
allowedInstances[Instance1.ID] = Instance1::getIntance
allowedInstances[Instance2.ID] = Instance2::getIntance
allowedInstances[Instance4.ID] = Instance4::getIntance
But it fails with:
Type mismatch.
Required: Instanceable.(Instanceable.Config) → Instanceable
Found: KFunction2<Implementation1, Instanceable.Config, Instanceable>
If I create the hashmap directly and let the compiler infer the types like this:
private val allowedInstances = mutableHashMapOf(
Implementation1.ID to Implementation1::getIntance,
Implementation2.ID to Implementation2::getIntance,
Implementation4.ID to Implementation4::getIntance,
)
Checking the type of the hashmap shows:
HashMap<String, out KFunction2<Nothing, Instanceable.Config, Instanceable>>
In fact I can do:
private val allowedInstances = mutableHashMapOf<String, Nothing.(Instanceable.Config)->Instanceable>()
allowedInstances[Instance1.ID] = Instance1::getIntance
allowedInstances[Instance2.ID] = Instance2::getIntance
allowedInstances[Instance4.ID] = Instance4::getIntance
So the actual question is:
Why the function of the second hashMap parameter has Nothing as the receptor? Why I cannot have the interface Instanceable instead?
Edit: Still not good to have the Nothing there:
allowedInstances["INSTANCE_1"]?.let{ it.invoke(Nothing, someConfig) }
//Fails with: Classifier 'Nothing' does not have a companion object, and thus must be initialized here
Edit 2: All of the errors are in compile time
Your function type
Instanceable.(Instanceable.Config) -> Instanceable
is describing an extension function on an instance of Instanceable. You need to omit the receiver from the function type to be able to match your constructors' signature:
(Instanceable.Config) -> Instanceable
Edit: The other half of the problem is that you define getInstance() as a member function of the class. So you have to create an invalid instance of your class to use to create a valid instance, which doesn't make sense.
I would delete the getInstance() function from your interface, and put the equivalent code in the constructor of your class. Then you can define a function type in your Map that constructs your items.
interface Instanceable {
data class Config(
val bar: Whatever
)
// REMOVE this: fun getIntance(config: Config): Instanceable
}
class Implementation1(config: Config) : Instanceable {
companion object {
const val ID = "INSTANCE_1"
}
private val foo: Whatever = config.bar
}
private val allowedInstances = mutableHashMapOf<String, (Instanceable.Config)->Instanceable>()
allowedInstances[Instance1.ID] = ::Implementation1
// and so on...
// If there's an implementation that has no config, you can use a lambda:
class NoConfigImplementation : Instanceable {
companion object {
const val ID = "INSTANCE_2"
}
}
allowedInstances[NoConfigImplementation.ID] = { _ -> NoConfigImplementation() }
I have below class, I would like to make this class a sealed class. Can you please help me as I am new to Kotlin.
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
#JsonSubTypes(
JsonSubTypes.Type(value = A:class, name = "PIZZA"),
JsonSubTypes.Type(value = B::class, name = "DONUT"),
JsonSubTypes.Type(value = C::class, name = "ICECREAM"),
JsonSubTypes.Type(value = D::class, name = "CHOCOLATE"),
)
open class food (var type: foodType, var quantity : String) {
open val taste : String=""
}
How to make this a sealed class perhaps a subclass of a sealed class, and how to instantiate it?
The foodType is enum class
enum class foodType {
PIZZA,
DONUT,
ICECREAM,
CHOCOLATE
}
I have the following based on the other post, but I am confused on passing the right parameters. Can someone help me understand what parameter I need to pass??
sealed class food (var type: foodType, var quantity: String) {
class favFood(taste: String): food(?, ?)
}
What is a sealed class ?
When you create a sealed class, you only allow the implementations you
created, just like for an enum (Only the constants you added are allowed). Once the module is compiled, you can't add any additional implementation anymore (in opposite to an open class).
Here is the link to the Kotlin documentation about sealed classes : https://kotlinlang.org/docs/sealed-classes.html
Sealed classes are interesting when you want to restrict the implementations
to a strict proposition. It can be the case with your use case, to restrict the jsonSubTypes you allow (others wouldn't be mapped).
How to transform an open class to a sealed class ?
So to transform your open class to a sealed class, you generally just need to change the keyword open to sealed. However, you also need to understand how the inheritance mechanism work with sealed classes.
For your example
With JsonSubType, you just need to map the property type to an implementation of your sealed class using a constant of your choice.
Also, you have to provide the values to your sealed class' properties when you extend it, so when you create your implementations.
In the next example, you can find how to give a value to your sealed class properties and what will be the result when you map it to json using JSonSubType :
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
#JsonSubTypes(
JsonSubTypes.Type(value = Pizza::class, name = "Pizza"),
JsonSubTypes.Type(value = Donut::class, name = "DonutDesert"), // As you can see, name is a value you give, not always need to be the class name
JsonSubTypes.Type(value = IceCream::class, name = "IceCream")
)
sealed class Food(val taste: String)
class Pizza(val size: PizzaSize, taste: String) : Food(taste) {
enum class PizzaSize {
SMALL,
MEDIUM,
LARGE
}
}
class Donut(val glaze: String, taste: String) : Food(taste)
class IceCream(val servings: Int, taste: String) : Food(taste)
class Basket(foods: List<Food>)
/* If you map a Basket to JSON, it will give you this :
{ foods: [
{ "type": "Pizza", "size": "MEDIUM", "taste": "Hawaii" },
{ "type": "DonutDesert", "glaze": "Sugar & Marshmallows", "taste" : "chocolate"},
{ "type": "IceCream", "servings": 3, "taste": "Strawberry" }
]}
*/
The Problem
Due to project architecture, backward compatibility and so on, I need to change class discriminator on one abstract class and all classes that inherit from it. Ideally, I want it to be an enum.
I tried to use #JsonClassDiscriminator but Kotlinx still uses type member as discriminator which have name clash with member in class. I changed member name to test what will happen and Kotlinx just used type as discriminator.
Also, outside of annotations, I want to avoid changing these classes. It's shared code, so any non backward compatible changes will be problematic.
Code
I prepared some code, detached from project, that I use for testing behavior.
fun main() {
val derived = Derived("type", "name") as Base
val json = Json {
prettyPrint = true
encodeDefaults = true
serializersModule = serializers
}.encodeToString(derived)
print(json)
}
#Serializable
#JsonClassDiscriminator("source")
data class Derived(
val type: String?,
val name: String?,
) : Base() {
override val source = FooEnum.A
}
#Serializable
#JsonClassDiscriminator("source")
abstract class Base {
abstract val source: FooEnum
}
enum class FooEnum { A, B }
internal val serializers = SerializersModule {
polymorphic(Base::class) {
subclass(Derived::class)
}
}
If I don't change type member name, I got this error:
Exception in thread "main" java.lang.IllegalArgumentException:
Polymorphic serializer for class my.pack.Derived has property 'type'
that conflicts with JSON class discriminator. You can either change
class discriminator in JsonConfiguration, rename property with
#SerialName annotation or fall back to array polymorphism
If I do change the name, I got this JSON which clearly shows, that json type discriminator wasn't changed.
{
"type": "my.pack.Derived",
"typeChanged": "type",
"name": "name",
"source": "A"
}
Kotlinx Serialization doesn't allow for significant customisation of the default type discriminator - you can only change the name of the field.
Encoding default fields
Before I jump into the solutions, I want to point out that in these examples using #EncodeDefault or Json { encodeDefaults = true } is required, otherwise Kotlinx Serialization won't encode your val source.
#Serializable
data class Derived(
val type: String?,
val name: String?,
) : Base() {
#EncodeDefault
override val source = FooEnum.A
}
Changing the discriminator field
You can use #JsonClassDiscriminator to define the name of the discriminator
(Note that you only need #JsonClassDiscriminator on the parent Base class, not both)
However, #JsonClassDiscriminator is more like an 'alternate name', not an override. To override it, you can set classDiscriminator in the Json { } builder
val mapper = Json {
prettyPrint = true
encodeDefaults = true
serializersModule = serializers
classDiscriminator = "source"
}
Discriminator value
You can change the value of type for subclasses though - use #SerialName("...") on your subclasses.
#Serializable
#SerialName("A")
data class Derived(
val type: String?,
val name: String?,
) : Base()
Including the discriminator in a class
You also can't include the discriminator in your class - https://github.com/Kotlin/kotlinx.serialization/issues/1664
So there are 3 options.
Closed polymorphism
Change your code to use closed polymorphism
Since Base is a sealed class, instead of an enum, you can use type-checks on any Base instance
fun main() {
val derived = Derived("type", "name")
val mapper = Json {
prettyPrint = true
encodeDefaults = true
classDiscriminator = "source"
}
val json = mapper.encodeToString(Base.serializer(), derived)
println(json)
val entity = mapper.decodeFromString(Base.serializer(), json)
when (entity) {
is Derived -> println(entity)
}
}
#Serializable
#SerialName("A")
data class Derived(
val type: String?,
val name: String?,
) : Base()
#Serializable
sealed class Base
Since Base is now sealed, it's basically the same as an enum, so there's no need for your FooEnum.
val entity = mapper.decodeFromString(Base.serializer(), json)
when (entity) {
is Derived -> println(entity)
// no need for an 'else'
}
However, you still need Json { classDiscriminator= "source" }...
Content-based deserializer
Use a content-based deserializer.
This would mean you wouldn't need to make Base a sealed class, and you could manually define a default if the discriminator is unknown.
object BaseSerializer : JsonContentPolymorphicSerializer<Base>(Base::class) {
override fun selectDeserializer(element: JsonElement) = when {
"source" in element.jsonObject -> {
val sourceContent = element.jsonObject["source"]?.jsonPrimitive?.contentOrNull
when (
val sourceEnum = FooEnum.values().firstOrNull { it.name == sourceContent }
) {
FooEnum.A -> Derived.serializer()
FooEnum.B -> error("no serializer for $sourceEnum")
else -> error("'source' is null")
}
}
else -> error("no 'source' in JSON")
}
}
This is a good fit in some situations, especially when you don't have a lot of control over the source code. However, I think this is pretty hacky, and it would be easy to make a mistake in selecting the serializer.
Custom serializer
Alternatively you can write a custom serializer.
The end result isn't that different to the content-based deserializer. It's still complicated, and is still easy to make mistakes with. For these reasons, I won't give a complete example.
This is beneficial because it provides more flexibility if you need to encode/decode with non-JSON formats.
#Serializable(with = BaseSerializer::class)
#JsonClassDiscriminator("source")
sealed class Base {
abstract val source: FooEnum
}
object BaseSerializer : KSerializer<Base> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Base") {
// We have to write our own custom descriptor, because setting a custom serializer
// stops the plugin from generating one
}
override fun deserialize(decoder: Decoder): Base {
require(decoder is JsonDecoder) {"Base can only be deserialized as JSON"}
val sourceValue = decoder.decodeJsonElement().jsonObject["source"]?.jsonPrimitive?.contentOrNull
// same logic as the JsonContentPolymorphicSerializer...
}
override fun serialize(encoder: Encoder, value: Base) {
require(encoder is JsonEncoder) {"Base can only be serialized into JSON"}
when (value) {
is Derived -> encoder.encodeSerializableValue(Derived.serializer(), value)
}
}
}
I am having the following problem in Kotlin when deserializing a LocationGeneric with Jackson. It is a case when I add no extra info to the abstract class I use to construct the concrete classes. It works good when I deserialize LocationOne or LocationTwo.
This is the code I have written:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY,
property = "type", visible = true)
#JsonSubTypes(
JsonSubTypes.Type(value = LocationOne::class, name = "ONE"),
JsonSubTypes.Type(value = LocationTwo::class, name = "TWO"),
JsonSubTypes.Type(value = LocationGeneric::class, name = "GENERIC_1"),
JsonSubTypes.Type(value = LocationGeneric::class, name = "GENERIC_2")
)
abstract class Location(
val type: String
)
class LocationGeneric(
type: String
): Location(type)
class LocationOne(
type: String,
val somethingSpecific: String
): Location(type)
class LocationAirport(
type: String,
val somethingElse: String
): Location(type)
This is the error I am getting:
Cannot construct instance of Location (although at least one Creator
exists): cannot deserialize from Object value (no delegate- or
property-based Creator)
I tried changing the abstract class to an open class, but had no luck so far. I works for the other cases. Why doesn't it find the default Kotlin constructor in the LocationGeneric case? Any ideas?
The problem I had was that I somehow Jackson lost visibility of the constructor, so I provided a default implementation for the generic cases and annotated the constructor and attributes of my generic implementation.
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY,
property = "type", visible = true, defaultImpl = LocationGeneric::class)
#JsonSubTypes(
JsonSubTypes.Type(value = LocationOne::class, name = "ONE"),
JsonSubTypes.Type(value = LocationTwo::class, name = "TWO")
)
abstract class Location(
val type: String
)
class LocationGeneric #JsonCreator constructor(
#JsonProperty("type") type: String
): Location(type)
This also allows me to add an init to handle weird mapping cases.