Nested property delegation in Kotlin - kotlin

As mentioned in the official tutorial, we can store properties in a Map and delegate a class to it:
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
However, sometimes we store non-trivial structures in a map, like another class (this is usual when working with complicated jsons). To better elaborate my idea, I came up with a pseudo-code like this:
class User(val map: Map<String, Any?>) {
val name: String by map
val otherType: OtherType by map
}
class OtherType {}
Is it possible to delegate such nested structure?

No problem, you can do this. It works:
fun main(args: Array<String>) {
val user = User(mapOf("name" to OtherType(1)))
println(user)
}
data class User(val map: Map<String?, Any?>) {
val name: String by map
val otherType: OtherType by map
}
data class OtherType(val something:Int) {}
You can delegate any type you want.

Related

Proper way to serialize a sealed class with kotlinx-serialization

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.

Serialize enum field into JSON in Kotlin

I've got a stupid question that stunned me a bit.
I have an enum and a data class like this:
enum class MyEventType(val typeName: String) {
FIRST("firstEventReceived")
}
data class MyEvent(
val id: String,
val event: MyEventType
)
I need to send this as a json string, but common desearilizer makes such a json
{
"id": "identifier",
"event": "FIRST"
}
but i need
{
"id": "identifier",
"event": "firstEventReceived"
}
As far as i understand, kotlin allows to override getter in data class, but i didn't succeed in it... Trying to make
data class MyEvent(
val id: String
) {
val event: MyEventType get() event.typeName
}
but i've missed something, i guess...
The simplest way is probably to annotate the property with #JsonValue:
enum class MyEventType(#JsonValue val typeName: String) {
FIRST("firstEventReceived")
}
data class MyEvent(
val id: String,
val event: MyEventType
)
fun main() {
MyEvent(id = "foo", event = MyEventType.FIRST)
.let { jacksonObjectMapper().writeValueAsString(it) }
.let { println(it) }
}
Prints:
{"id":"foo","event":"firstEventReceived"}
The easiest way is to annotate the typeName with #JsonValue. This will serialise and deserialise the enum field as you want.
enum class MyEventType(#JsonValue val typeName: String) {
FIRST("firstEventReceived");
}
An alternative is to use #JsonFormat (if you are using jackson version < 2.9);
enum class MyEventType(#JsonFormat(shape = JsonFormat.Shape.OBJECT) val typeName: String) {
FIRST("firstEventReceived");
}
Herer's an example;
#JvmStatic
fun main(args: Array<String>) {
val mapper = jacksonObjectMapper()
val json = mapper.writeValueAsString(MyEvent("1", MyEventType.FIRST))
println(json)
val event = mapper.readValue<MyEvent>(json)
println(event)
}
You get the output;
{"id":"1","event":"firstEventReceived"}
MyEvent(id=1, event=FIRST)
I used Jackson version 2.12.0. Here's a good read on enum manipulation with Jackson - https://www.baeldung.com/jackson-serialize-enums
Also you can have enum with 2+ fields which you want to be serialized
enum class MyEventType(
val firstField: String,
val secondField: String,
val thirdField: String
) {
MY_ENUM("firstFieldValue", "secondFieldValue", "thirdFieldValue")
}
You can chose one of the following two options:
Put #JsonValue over a method(lets call it getter) that will return the required value(if you need only part of the fields):
#JsonValue
fun getSerializedObject(): String {
return "{firstField: $firstField, thirdField: $thirdField}"
}
Result will be "{firstField: firstFieldValue, thirdField: thirdFieldValue}"
Put #JsonFormat(shape = JsonFormat.Shape.OBJECT) over your enum class(for serialization class as common class):
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
enum class MyEventType(
val firstField: String,
val secondField: String,
val thirdField: String
) {
MY_ENUM("firstField", "secondField", "thirdField")
}
Result will be "{"firstField": "firstFieldValue", "secondField": "secondFieldValue", "thirdField": "thirdFieldValue"}"
For GSON users, you can use the #SerializedName annotation:
enum class ConnectionStatus {
#SerializedName("open")
OPEN,
#SerializedName("connecting")
CONNECTING,
#SerializedName("closed")
CLOSED
}

Kotlinx Serializer and single-value domain types

I love to use single-value types for e.g. IDs. How can I tell Kotlinx Serializer to consider such types as regular fields, not objects?
Example:
data class User(
val id: UserId,
val name: String,
)
should be serialized to:
{
id: 123,
name: "foo"
}
and UserId is just data class(val value: Int)?
EDIT: I guess I could use custom serializer for each Id class, but I really don't wanna repeat such code for each and every ID in my domain. Moreover, I am not sure how to write one; so far my effort is failing.
EDIT2: this is my attempt for writing such serializer:
open class DoctorIdSerializer : KSerializer<DoctorId> {
override val descriptor: SerialDescriptor = PrimitiveDescriptor("id", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: DoctorId) {
encoder.encodeInt(value.value)
}
override fun deserialize(decoder: Decoder): DoctorId {
return DoctorId(decoder.decodeInt())
}
}
basically, whats written here but its not working :(

Property delegation by Map for open hierarchies

Consider I have a base of an open hierarchy where all the properties are delegated to a single map:
interface JsonExternalizable {
fun serialize(): MutableMap<String, JsonNode>
}
abstract class AbstractJsonExternalizable protected constructor(private val map: Map<String, Any?>) : JsonExternalizable {
override fun serialize(): MutableMap<String, JsonNode> {
// ...
}
}
Now a sample immutable implementation (I want to have an additional secondary constructor where the list of arguments would "mirror" the type's corresponding properties):
open class Bean1 protected constructor(map: Map<String, Any?>) : AbstractJsonExternalizable(map) {
constructor(property1: String,
property2: Int,
property3: Boolean) : this(mapOf(Bean1::property1 to property1,
Bean1::property2 to property2,
Bean1::property3 to property3).mapKeys { it.key.name })
val property1: String by map
val property2: Int by map
val property3: Boolean by map
}
Things get worse for more "distant" descendants, as I have to list every supertype property (along with the own properties) in the secondary constructor:
class Bean3 private constructor(map: Map<String, Any?>) : Bean1(map) {
val propertyX: Array<Byte> by map
constructor(property1: String,
property2: Int,
property3: Boolean,
propertyX: Array<Byte>) : this(mapOf(Bean3::property1 to property1,
Bean3::property2 to property2,
Bean3::property3 to property3,
Bean3::propertyX to propertyX).mapKeys { it.key.name })
}
Is there a more elegant way to express the same idea, with less boilerplate code?

Access properties of a subclass of the declared object type

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.