What is the difference for each case of this sealed class? - kotlin

While looking at the sample code to use the sealed class, I saw several cases in which the method of using the sealed class was different.
case1 is similar to enum class, but I know that it can create multiple instances.
But what makes case2 different?
And I'm wondering what's the difference between inheriting from a normal class(or interface)
case1
The first case is the most common sealed class method in the sample code.
sealed class Parent(
val t1: String,
val t2: String,
) {
data class A(
val id: String,
val title: String,
val num : Int
) : Parent(
t1 = id,
t2 = title,
) { }
data class B(
val id: String,
val title: String,
) : Parent(
t1 = id,
t2 = title,
) { }
}
case2
The second case is a case where you are curious about the difference from inheriting a normal class.
sealed class Parent(
val t1: String,
open val t2: String,
) { }
data class A(
val id: String,
val title: String,
val num : Int
) : Parent(
t1 = id,
t2 = title,
) { }
data class B(
val id: String,
val title: String,
) : Parent(
t1 = id,
t2 = title,
) { }
open class Parent( // or interface
val t1: String,
open val t2: String,
) { }
data class A(
val id: String,
val title: String,
val num : Int
) : Parent(
t1 = id,
t2 = title,
) { }
data class B(
val id: String,
val title: String,
) : Parent(
t1 = id,
t2 = title,
) { }

Case 1:
uses nested classes.
was the required way to do sealed classes in Kotlin before 1.5.
suggests a direct relationship between the nessted and outer class that you might want to carry throughout your code for clarity (like PaymentType.CreditCard, PaymentType.Checking, etc), so can be used as a grouping / organizing strategy
requires scoping when declaring fields (val paymentMethod = PaymentMethod.CreditCard(...)) or adding extra imports
Case 2:
no more nested classes
only possible as of Kotlin 1.5
suggests a less direct relationship between the base and derived classes that doesn't require being maintained throughout the code (like Animal, Cat, Dog, etc)
Does not require scoping or extra imports (val cat = Cat())
Finally:
And I'm wondering what's the difference between inheriting from a normal class(or interface)
The Kotlin docs are pretty clear on sealed classes and regular classes. The key point for sealed classes being:
Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance. All direct subclasses of a sealed class are known at compile time. No other subclasses may appear after a module with the sealed class is compiled. For example, third-party clients can't extend your sealed class in their code. Thus, each instance of a sealed class has a type from a limited set that is known when this class is compiled.
In short: they're enums on crack. (PSA: don't do crack).

Related

Serialization with sealed classes fails in Kotlin serialization

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.

Spring Data Neo4J Projections without cyclic dependency lead to error if domain contains cyclic dependency

I have Nodes similar to this:
#Node
data class Actor (
#Id val id: UUID,
val name: String,
#Relationship(type = "ACTED_IN")
val films: Set<Film> = emptySet())
#Node
data class Film(
#Id val id: UUID,
val name: String,
#Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)
val actors: Set<Actor> = emptySet())
Further more i have projection classes:
data class ActorProjection (
val name: String,
val films: Set<FilmProjection>)
data class FilmProjection(
val name: String)
Having a repository function as such:
#Repository
interface ActorRepository : Neo4jRepository<Actor, UUID> {
fun findByName(name: String): ActorProjection?
}
Using the function produces an exception:
The node with id 0 has a logical cyclic mapping dependency. Its creation caused the creation of another node that has a reference to this.
So I guess the domain class is used instead of the projection class to create the queries. How can I deal with this? Ofc I could delete the inverse Relationship in the Film class. However, I would like to have all associations visible in both classes for documentation and to also achieve the inverse projection. How to achieve this?
Edit:
Changing the node classes as such:
#Node
data class Actor (
#Id val id: UUID = UUID.randomUUID(),
val name: String
){
#Relationship(type = "ACTED_IN")
var films: Set<Film> = emptySet()
private set
}
#Node
data class Film(
#Id val id: UUID = UUID.randomUUID(),
val name: String) {
#Relationship(type = "ACTED_IN", direction = Relationship.Direction.INCOMING)
var actors: Set<Actor> = emptySet()
private set
}
I get rid of the exception. However the resulting ActorProjection has a Set of Films not FilmProjections when looking at it with the debugger. I guess its due to the generic type erasure. Is there a annotation or anything to give the type hint to neo4j?

can I use kotlinx serializer with multiple sealed class levels as parents and a nested invocation?

I am trying to use kotlinx #Serializable and Ive faced this issue:
I have the following classes:
#Serializable
sealed class GrandParent
a second one:
#Serializable
sealed class Parent() : GrandParent() {
abstract val id: String
}
and a third one
#Serializable
data class Child(
override val id: String, ....
): Parent()
I'm needing of grandparent since I use it as a generic type in another class, which happen to also have a reference to the GrandParent class
#Serializable
data class MyContent(
override val id: String,
....
val data: GrandParent, <- so it has a self reference to hold nested levels
...): Parent()
Every time I try to run this I get an error...
Class 'MyContent' is not registered for polymorphic serialization in the scope of 'GrandParent'.
Mark the base class as 'sealed' or register the serializer explicitly.
I am using ktor as wrapper, kotlin 1.5.10. I did this based on https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#registered-subclasses
Any ideas?
You should serialize and deserialize using your sealed class in order for kotlin serialization to "know" to add a discriminator with the right implementation. By default it search for type in the json but you can change it with JsonBuilder:
Json {
classDiscriminator = "class"
}
Here is an example:
#Serializable
sealed class GrandParent
#Serializable
sealed class Parent : GrandParent() {
abstract val id: String,
}
#Serializable
data class Child(
override val id: String,
): Parent()
#Serializable
data class MyContent(
override val id: String,
val data: GrandParent,
): Parent()
fun main() {
val test = MyContent(id = "test", data = Child(id = "child"))
val jsonStr = Json.encodeToString(GrandParent.serializer(), test)
println("Json string: $jsonStr")
val decoded = Json.decodeFromString(GrandParent.serializer(), jsonStr)
println("Decoded object: $decoded")
}
Result in console:
Json string: {"type":"MyContent","id":"test","data":{"type":"Child","id":"child"}}
Decoded object: MyContent(id=test, data=Child(id=child))
encode and decode can also be written like this (but behind the scenes it will use reflections):
val jsonStr = Json.encodeToString<GrandParent>(test)
println("Json string: $jsonStr")
val decoded = Json.decodeFromString<GrandParent>(jsonStr)
println("Decoded object: $decoded")

What are the best practices to unify access to data classes properties

I have a class ExpenseDto
data class ExpenseDto(
val id: Int,
val name: String,
val aggregationA: ExpenseAggregationA,
val aggregationB: ExpenseAggregationB,
val aggregationC: ExpenseAggregationC,
)
And all its associations have the same fields. And what are the best practies that can be applied here to universalize it
data class ExpenseAggregationA(
val id: Int,
val text: String? = null
)
data class ExpenseAggregationB(
val id: Int,
val text: String? = null
)
data class ExpenseAggregationC(
val id: Int,
val text: String? = null
)
data classes can inherit from a sealed class.
sealed class ExpenseAggParent (val id: Int, val text: String? = null) {
data class ExpAggA( override val id: Int, override ...)
data class ...
data class ...
}
Besides that, sometimes I like to have "property unifier" interfaces, especially when refactoring a legacy code with classes with the same semantics but different names of the properties:
interface HasId(val id: Int)
data class ExpenseAggregationX(
override val id: Int,
...
): HasId
data class ExpenseAggregationY: HasId {
val someOtherNameButStillId: Int
override val id: Int
get() = this.someOtherNameButStillId
}
And, of course, you can use the good old interface, but for that, you may re-use the above approach:
interface ExpenseAggregation: HasId, HasText
data class ExpenseAggregationA(
override val id: Int,
...
): ExpenseAggregation
// Other approach for classes with other existing names, see above
data class ExpenseAggregationB: ExpenseAggregation {
...
}
This is close to a mix-in approach which Kotlin does not currently support directly, but for completeness, there are delegations usable if your class is rather a service than a DTO.

Wrapping data classes in kotlin

I wanted to ask about data classes in kotlin.
Suppose I have 2 data classes , data class A and data class B , I want to wrap my data class A around data class B.
Is this a case of Inheritance or inline class , I am kind of new to Kotlin .
data class A(
var x1:Float?,
var y1:Float?
)
data class B(
var x:Float?,
var y:Float?,
// other variables and methods
)
Thanks
You can't inherit a Kotlin data class from an other data class, but you can use interfaces or abstract classes and your data classes can implement/extend these:
abstract class A {
abstract var valueA: Int
}
interface B {
val valueB: Int
}
data class C(
val value2: String,
override var valueA: Int
) : A()
data class D(
override val valueB: Int,
val valueD: String
) : B