I am having issues with spring data (elasticsearch) and Kotlin sealed classes. It seems it's detecting two id mappings. The classes:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "#type")
#JsonSubTypes(
JsonSubTypes.Type(value = AbstractSectionV1Dto.HtmlSectionV1Dto::class, name = "HTML"))
sealed class AbstractSectionV1Dto(
open val number: Int?,
open val id: Long?,
open val questionAnswers: List<AbstractQuestionAnswerV1Dto>,
open val rightQuestionAnswers: Int?,
open val questionAnswersSize: Int?) : Dto {
data class HtmlSectionV1Dto(
override val number: Int?,
override val id: Long,
override val questionAnswers: List<AbstractQuestionAnswerV1Dto>,
override val rightQuestionAnswers: Int?,
override val questionAnswersSize: Int?,
val html: String?) : AbstractSectionV1Dto(number, id, questionAnswers, rightQuestionAnswers, questionAnswersSize)
(...)
}
when I save the document to elasticsearch, I get
org.springframework.data.mapping.MappingException: Attempt to add id property private final java.lang.Long com.package.AbstractSectionV1Dto.id but already have property private final long com.package.AbstractSectionV1Dto$TextSectionV1Dto.id registered as id. Check your mapping configuration!
I also tried setting #Id only on the top class and on both. Any clues?
I think I got it working by placing org.springframework.data.annotation.Transient in the sealed class id, but it would be nice if someone could confirm or say something about the best solution to this problem.
Related
The Code A is from the official sample project.
I find override is added before val route in data class TopLevelDestination, but it's hard to understand to instance the interface NiaNavigationDestination in this way.
Is there other way to instance the interface NiaNavigationDestination ?
Code A
data class TopLevelDestination(
override val route: String,
override val destination: String,
val selectedIcon: Icon,
val unselectedIcon: Icon,
val iconTextId: Int
) : NiaNavigationDestination
interface NiaNavigationDestination {
val route: String
val destination: String
}
You can override the vals from the interface in the body of the class, but then they will not participate in equals(), hashcode(), or copy(). A data class only uses its constructor parameters for those functions.
Example:
data class TopLevelDestination(
val selectedIcon: Icon,
val unselectedIcon: Icon,
val iconTextId: Int
) : NiaNavigationDestination {
override val route: String = "something"
override val destination: String = "something else"
}
In this case, it does not seem productive for something like "destination" to not be a part of equals() for a class that has "destination" in it's name!
It also becomes impossible to set these values to anything but a constant or something that depends on the other constructor properties.
I have a simple hierarchy containing of the following:
abstract class BaseItem
open class Item : BaseItem
class Backpack : Item
They should all work with Kotlinx Serialization. It went fine until I added the Backpack class.
I use version 1.4.32 of Kotlinx Serialization.
Here is my class hierarchy in detail
// Items.kt
#Serializable
sealed class BaseItem {
abstract val id: String
abstract val type: ItemType
abstract var brand: String
abstract var model: String
abstract var imageLink: String
abstract var traits: MutableList<Trait>
abstract var implicitTraits: MutableList<Trait>
abstract var details: MutableMap<String, String>
}
#Serializable
open class Item(
override val id: String = UUID.randomUUID().toString(),
override val type: ItemType = ItemType.UNDEFINED,
override var brand: String,
override var model: String,
override var imageLink: String,
override var traits: MutableList<Trait>,
override var implicitTraits: MutableList<Trait>,
override var details: MutableMap<String, String>,
) : BaseItem()
#Serializable // is marked as incorrect
class Backpack(
brand: String,
model: String,
imageLink: String,
traits: MutableList<Trait>,
implicitTraits: MutableList<Trait>,
details: MutableMap<String, String>,
var volume: Int
) : Item(
type = ItemType.BACKPACK,
brand = brand,
model = model,
imageLink = imageLink,
traits = traits,
implicitTraits = implicitTraits,
details = details
)
The IDE is showing me the following message for the #Serialization annotation attached to the Backpack class.
This class is not serializable automatically because it has primary constructor parameters that are not properties
I was not able to find out what the working way it is to make this correct
It's because the parameters of your constructor are not defined as properties of the class. To have the parameters defined as properties you have to add val or var to the parameters. This would resolve the error message you currently have:
#Serializable
class Backpack(
override var brand: String,
override var model: String,
override var imageLink: String,
override var traits: MutableList<Trait>,
override var implicitTraits: MutableList<Trait>,
override var details: MutableMap<String, String>,
var volume: Int
) : Item(
type = ItemType.BACKPACK,
brand = brand,
model = model,
imageLink = imageLink,
traits = traits,
implicitTraits = implicitTraits,
details = details
)
But this would still not compile as you would end up with Serializable class has duplicate serial name of property 'brand', either in the class itself or its supertypes for all properties that are used in both classes. But anyhow, I am a little bit surprised by the design as it usually not a good practice to inherit from a non abstract class. Without knowing the intentions I am wondering whether sth.like this would not work for you as well:
sealed class BaseItem {
abstract val id: String
abstract val type: ItemType
abstract var brand: String
abstract var model: String
abstract var imageLink: String
abstract var traits: MutableList<Trait>
abstract var implicitTraits: MutableList<Trait>
abstract var details: MutableMap<String, String>
}
#Serializable
data class Item(
override val id: String = UUID.randomUUID().toString(),
override val type: ItemType = ItemType.UNDEFINED,
override var brand: String,
override var model: String,
override var imageLink: String,
override var traits: MutableList<Trait>,
override var implicitTraits: MutableList<Trait>,
override var details: MutableMap<String, String>,
) : BaseItem()
#Serializable
data class Backpack(
override val id: String = UUID.randomUUID().toString(),
override val type: ItemType = ItemType.BACKPACK,
override var brand: String,
override var model: String,
override var imageLink: String,
override var traits: MutableList<Trait>,
override var implicitTraits: MutableList<Trait>,
override var details: MutableMap<String, String>,
override var var volume: Int
) : BaseItem()
I btw. removed the #Serializable from BaseItem as it is unnecessary because the class is abstract anyhow and therefore won't be serialized at all. I also made your classes data class because my impression was that those basically exist to hold data and those are usually implemented with data class. I left the many var I see as I don't know the reasoning for those but a small hint from my side is that you should prefer val over var especially in data class. A var in this scenario feels like a code smell to me and you might want to consider using val instead. A good literatur for such things is the Kotlin page itself: https://kotlinlang.org/docs/coding-conventions.html#idiomatic-use-of-language-features
From Designing Serializable Hierarchy:
To make hierarchy of classes serializable, the properties in the parent class have to be marked abstract, making the [parent] class abstract, too.
I am not sure if it is possible yet but i would like to serialize the following class.
#Serializable
sealed class RestResponseDTO<out T : Any>{
#Serializable
#SerialName("Success")
class Success<out T : Any>(val value: T) : RestResponseDTO<T>()
#Serializable
#SerialName("Failure")
class Error(val message: String) : RestResponseDTO<String>()
}
when i try and use it
route(buildRoute(BookDTO.restStub)) {
get {
call.respond(RestResponseDTO.Success(BookRepo.getAll()))
}
}
I get this error:
kotlinx.serialization.SerializationException: Serializer for class
'Success' is not found. Mark the class as #Serializable or provide the
serializer explicitly.
The repo mentioned in the get portion of the route returns a list of BookDTO
#Serializable
data class BookDTO(
override val id: Int,
override val dateCreated: Long,
override val dateUpdated: Long,
val title: String,
val isbn: String,
val description: String,
val publisher:DTOMin,
val authors:List<DTOMin>
):DTO {
override fun getDisplay() = title
companion object {
val restStub = "/books"
}
}
This problem is not a deal breaker but it would be great to use an exhaustive when on my ktor-client.
Serializing sealed classes works just fine. What is blocking you are the generic type parameters.
You probably want to remove those, and simply use value: DTO. Next, make sure to have all subtypes of DTO registered for polymorphic serialization in the SerializersModule.
Having following interface and class:
interface IUserCreated {
val userCreated: Boolean
}
#Entity
data class Test(
#PrimaryKey #ColumnInfo(name = "test_id") var id: Long,
#ColumnInfo(name = "user_created") override val userCreated: Boolean
) : IUserCreated
This leads to following error:
error: Ambiguous getter for Field(element=userCreated, name=userCreated, type=boolean, affinity=INTEGER, collate=null, columnName=user_created, defaultValue=null, parent=null, indexed=false, nonNull=true).
All of the following match: getUserCreated, isUserCreated. You can #Ignore the ones that you don't want to match.
Question
How can I use the #Ignore annotation in this example? The userCreated boolean field is part of the entity, I just want to ignore the unused getUserCreated and tell room that it should use the default kotlin getter function isUserCreated
Note
I know I can solve this by doing following:
interface IUserCreated {
val internalUserCreated: Boolean
fun getUserCreated(): Boolean = internalUserCreated
}
#Entity
data class Test(
#PrimaryKey #ColumnInfo(name = "test_id") var id: Long,
#ColumnInfo(name = "user_created") override val internalUserCreated: Boolean
) : IUserCreated
I wonder if I can somehow solve this without the workaround, I don't want to do this for every boolean field in any entity I use, but I don't know how I can use the #Ignore annotation to fix the problem.
I have the following abstract class:
abstract class AbstractBook {
abstract val type: String
abstract val privateData: Any
abstract val publicData: Any
}
and the following class which inherits the AbstactBook class:
data class FantasyBook (
override val type: String = "FANTASY",
override val privateData: FantasyBookPrivateData,
override val publicData: FantasyBookPublicData
) : AbstractBook()
And then there is this class which should include data from any type of AbstractBook:
data class BookState(
val owner: String,
val bookData: AbstractBook,
val status: String
)
If I have an instance of BookState, how do I check which type of Book it is and then access the according FantasyBookPrivateData, and FantasyBookPublicData variables?
I hope I described my issue well & thanks in advance for any help!
What you describe is a sealed class:
sealed class Book<T, K> {
abstract val type: String
abstract val privateData: T
abstract val publicData: K
data class FantasyBook(
override val type: String = "FANTASY",
override val privateData: String,
override val publicData: Int) : Book<String, Int>()
}
and in your data class you can do pattern matching like this:
data class BookState(
val owner: String,
val bookData: Book<out Any, out Any>,
val status: String) {
init {
when(bookData) {
is Book.FantasyBook -> {
val privateData: String = bookData.privateData
}
}
}
}
to access your data in a type-safe manner. This solution also makes type redundant since you have that information in the class itself.
I agree with #Marko Topolnik that this seems like a code smell, so you might want to rethink your design.
interface AbstractBook<T , U> {
val privateData: T
val publicData: U
}
data class FantasyBook (
override val privateData: FantasyBookPrivateData,
override val publicData: FantasyBookPublicData
) : AbstractBook<FantasyBookPrivateData , FantasyBookPublicData>
data class BookState(
val owner: String,
val bookData: AbstractBook<*, *>,
val status: String
)
if(bookState.bookData is FantasyBook) {
// Do stuff
}
Creating a type variable is a weak type language writing style. You should use generic class.