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
}
Related
Currently I have multiple enums each with a userFriendly : string, e.g.
enum class TestGroup(val userFriendly: String) {
A("A"),
B("B")
}
For each enum I now have a separate function transforming them to a class SelectField:
class SelectField(
val value: String, /** The value which gets submitted e.g. a UUID or other identifier */
val text: String /** The human-friendly label e.g. the name of an organisation */
)
fun transformTestGroups(testGroups: Array<TestGroup>): List<SelectField> =
testGroups.map {
SelectField(
value = it.name,
text = it.userFriendly
)
}
I found that enums could implement an interface:
interface UserFriendly {
val userFriendly: String
}
enum class TestGroup(override val userFriendly: String) : UserFriendly {
A("A"),
B("B")
}
However, I don't know how to make a genericTransform(values:Array<Enum<UserFriendly>>):List<SelectField>. Is this possible in Kotlin?
Arrays in Kotlin are mutable and hence invariant. That's a fancy way of saying that an Array<Int> is not an Array<Any>. The reason for that is: If we could cast x: Array<Int> up to Array<Any>, then we could write x[0] = "ABC", since "ABC" is a valid Any, and now x would contain a value that is not an integer.
So simply using the supertype won't work here. But generics will. Specifically, we'll use the curiously recurring template pattern to require that our generic argument be an enum and a UserFriendly.
fun<T> transformTestGroups(testGroups: Array<T>): List<SelectField>
where T: UserFriendly,
T: Enum<T> =
// Same implementation as before :)
testGroups.map {
SelectField(
value = it.name,
text = it.userFriendly
)
}
Complete runnable example:
interface UserFriendly {
val userFriendly: String
}
enum class TestGroup(override val userFriendly: String): UserFriendly {
A("A"),
B("B")
}
data class SelectField(
val value: String,
val text: String
)
fun<T> transformTestGroups(testGroups: Array<T>): List<SelectField>
where T: UserFriendly,
T: Enum<T> =
testGroups.map {
SelectField(
value = it.name,
text = it.userFriendly
)
}
fun main(args: Array<String>) {
println(transformTestGroups(arrayOf(TestGroup.A, TestGroup.B)))
}
Try it online!
I used the data type java.util.UUID in my data models and I have used Moshi for serialization.
But I encountered an error saying that "Platform class java.util.UUID requires explicit JsonAdapter to be registered"
I have gone through the documentation of Moshi for writing custom adapters and I tried to replicate it accordingly.
I wrote an adapter and added it to a moshi instance. But still I encounter the same error .
Adapter
class UUIDAdapter {
#ToJson
fun toJson(value:java.util.UUID):java.util.UUID{
return value
}
#FromJson
fun fromJson(value: String):java.util.UUID{
return java.util.UUID.fromString(value)
}
}
Model
#JsonClass(generateAdapter = true)
data class AddWorkspace(
#Json(name = "user_id")
val user_id: UUID,
#Json(name = "name")
val name:String,
#Json(name = "descp")
val descp:String,
#Json(name = "created_at")
val created_at:String
)
Moshi
private val moshi = Moshi.Builder()
.add(UUIDAdapter())
.build()
private val retrofitBuilder = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(MoshiConverterFactory.create(moshi))
What else am I missing so that I can use the adapter correctly ?
Edit : Well, the methods toJson and fromJson are not being called in the first place. I tried to implement the JsonAdapter class and override the methods toJson and fromJson, but the issue I face here is that in case of the method toJson, I need to send a java.util.UUID value, but the JsonWriter cannot write a value of such data type.
Please suggest me a way to work my way through this. Thanks :)
UUID adapter
class UUIDAdapter:JsonAdapter<UUID>(){
#FromJson
override fun fromJson(reader: JsonReader): UUID? {
return UUID.fromString(reader.readJsonValue().toString())
}
#ToJson
override fun toJson(writer: JsonWriter, value: UUID?) {
writer.jsonValue(value)
}
}
You're so close. Change the #ToJson to this:
#ToJson
fun toJson(value:java.util.UUID): String {
return value.toString()
}
as Jesse described just use:
class UuidAdapter {
#FromJson
fun fromJson(uuid: String): UUID = UUID.fromString(uuid)
#ToJson
fun toJson(value: UUID): String = value.toString()
}
I'm trying to build a class where certain values are Observable but also Serializable.
This obviously works and the serialization works, but it's very boilerplate-heavy having to add a setter for every single field and manually having to call change(...) inside each setter:
interface Observable {
fun change(message: String) {
println("changing $message")
}
}
#Serializable
class BlahVO : Observable {
var value2: String = ""
set(value) {
field = value
change("value2")
}
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
}
println(BlahVO().apply { value2 = "test2" })
correctly outputs
changing value2
{"value2":"test2"}
I've tried introducing Delegates:
interface Observable {
fun change(message: String) {
println("changing $message")
}
#Suppress("ClassName")
class default<T>(defaultValue: T) {
private var value: T = defaultValue
operator fun getValue(observable: Observable, property: KProperty<*>): T {
return value
}
operator fun setValue(observable: Observable, property: KProperty<*>, value: T) {
this.value = value
observable.change(property.name)
}
}
}
#Serializable
class BlahVO : Observable {
var value1: String by Observable.default("value1")
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
}
println(BlahVO().apply { value1 = "test1" }) correctly triggers change detection, but it doesn't serialize:
changing value1
{}
If I go from Observable to ReadWriteProperty,
interface Observable {
fun change(message: String) {
println("changing $message")
}
fun <T> look(defaultValue: T): ReadWriteProperty<Observable, T> {
return OP(defaultValue, this)
}
class OP<T>(defaultValue: T, val observable: Observable) : ObservableProperty<T>(defaultValue) {
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
super.setValue(thisRef, property, value)
observable.change("blah!")
}
}
}
#Serializable
class BlahVO : Observable {
var value3: String by this.look("value3")
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
}
the result is the same:
changing blah!
{}
Similarly for Delegates.vetoable
var value4: String by Delegates.vetoable("value4", {
property: KProperty<*>, oldstring: String, newString: String ->
this.change(property.name)
true
})
outputs:
changing value4
{}
Delegates just doesn't seem to work with Kotlin Serialization
What other options are there to observe a property's changes without breaking its serialization that will also work on other platforms (KotlinJS, KotlinJVM, Android, ...)?
Serialization and Deserialization of Kotlin Delegates is not supported by kotlinx.serialization as of now.
There is an open issue #1578 on GitHub regarding this feature.
According to the issue you can create an intermediate data-transfer object, which gets serialized instead of the original object. Also you could write a custom serializer to support the serialization of Kotlin Delegates, which seems to be even more boilerplate, then writing custom getters and setters, as proposed in the question.
Data Transfer Object
By mapping your original object to a simple data transfer object without delegates, you can utilize the default serialization mechanisms.
This also has the nice side effect to cleanse your data model classes from framework specific annotations, such as #Serializable.
class DataModel {
var observedProperty: String by Delegates.observable("initial") { property, before, after ->
println("""Hey, I changed "${property.name}" from "$before" to "$after"!""")
}
fun toJson(): String {
return Json.encodeToString(serializer(), this.toDto())
}
}
fun DataModel.toDto() = DataTransferObject(observedProperty)
#Serializable
class DataTransferObject(val observedProperty: String)
fun main() {
val data = DataModel()
println(data.toJson())
data.observedProperty = "changed"
println(data.toJson())
}
This yields the following result:
{"observedProperty":"initial"}
Hey, I changed "observedProperty" from "initial" to "changed"!
{"observedProperty":"changed"}
Custom data type
If changing the data type is an option, you could write a wrapping class which gets (de)serialized transparently. Something along the lines of the following might work.
#Serializable
class ClassWithMonitoredString(val monitoredProperty: MonitoredString) {
fun toJson(): String {
return Json.encodeToString(serializer(), this)
}
}
fun main() {
val monitoredString = obs("obsDefault") { before, after ->
println("""I changed from "$before" to "$after"!""")
}
val data = ClassWithMonitoredString(monitoredString)
println(data.toJson())
data.monitoredProperty.value = "obsChanged"
println(data.toJson())
}
Which yields the following result:
{"monitoredProperty":"obsDefault"}
I changed from "obsDefault" to "obsChanged"!
{"monitoredProperty":"obsChanged"}
You however lose information about which property changed, as you don't have easy access to the field name. Also you have to change your data structures, as mentioned above and might not be desirable or even possible. In addition, this work only for Strings for now, even though one might make it more generic though.
Also, this requires a lot of boilerplate to start with. On the call site however, you just have to wrap the actual value in an call to obs.
I used the following boilerplate to get it to work.
typealias OnChange = (before: String, after: String) -> Unit
#Serializable(with = MonitoredStringSerializer::class)
class MonitoredString(initialValue: String, var onChange: OnChange?) {
var value: String = initialValue
set(value) {
onChange?.invoke(field, value)
field = value
}
}
fun obs(value: String, onChange: OnChange? = null) = MonitoredString(value, onChange)
object MonitoredStringSerializer : KSerializer<MonitoredString> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("MonitoredString", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: MonitoredString) {
encoder.encodeString(value.value)
}
override fun deserialize(decoder: Decoder): MonitoredString {
return MonitoredString(decoder.decodeString(), null)
}
}
I have a JSON object that is something like
{
"tsp": "ABC" // can be only one of three things: "ABC", "DEF", "GHI"
"userId" : "lkajsdlk-199191-lkjdflakj"
}
Instead of writing a dataclass along the lines of
data class User(#SerializedName("tsp") val tsp: String, #SerializedName("userId") val userId: String
i'd like to have an enum that defines the three values so that my data class can be
data class User(#SerializedName("tsp") val tsp: TspEnum, #SerializedName("userId") val userId: String
I had tried writing an enum that was
enum class TspEnum(provider: String) {
AY_BEE_CEE("ABC"),
DEE_EE_EFF("DEF"),
GEE_HAYTCH_I("GHI");
}
however that did not work out
I've realized now that calling TspEnum.provider will return the value of the enum, but I'm not sure how to make GSON coƶperate whilst serializing the JSON into the kotlin data class
I've read that there is an issue with Kotlin typing and GSON here: https://discuss.kotlinlang.org/t/json-enum-deserialization-breakes-kotlin-null-safety/11670
however, the way that person is serializing the hair colours to map into an enum is different enough from my tsp json object to make me scratch my head.
Any pointers on where i'm going wrong would be great, cheers!
You can create a deserializer for TspEnum:
class TspDeserializer : JsonDeserializer<TspEnum> {
override fun deserialize(json: JsonElement, typeOfT: Type?, context: JsonDeserializationContext?): TspEnum {
val stringValue = json.asString
for (enum in TspEnum.values()) {
if (enum.provider == stringValue) {
return enum
}
}
throw IllegalArgumentException("Unknown tsp $stringValue!")
}
}
next you have to register it:
val gson = GsonBuilder()
.registerTypeAdapter(TspEnum::class.java, TspDeserializer())
.create()
and then you can parse your user:
val user = gson.fromJson(json, User::class.java)
println(user) // prints User(tsp=AY_BEE_CEE, userId=lkajsdlk-199191-lkjdflakj)
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.