How to combine polymorphic and transforming serializers for nested JSON, using Kotlin serialization? - kotlin

This is a simplified example of the JSON I want to work with:
{
"useless_info": "useless info",
"data": {
"useless_info2": "useless info 2",
"children": [
{
"kind": "Car",
"data": {
"id": 1,
"transmission": "manual"
}
},
{
"kind": "Boat",
"data": {
"id": 2,
"isDocked": true
}
}
]
}
}
children is an array of vehicle objects. vehicle can be boat or car.
My Problem
The information that I want is nested quite deep (the real JSON is much deeply nested). A hack solution is to model the JSON exactly by writing dozens of nested data classes that references each other. I do not want to do this.
My problem is that while I know how to use JsonTransformingSerializer to unwrap arrays of a single type, and JsonContentPolymorphicSerializer to work with objects of various types, in this situation I believe I require both, but I can't get it to work.
What I Did
Assuming a single type
I tried understanding how it would work if it was a single type.
If the objects I wanted where all of the same type, it would be trivial to implement a JsonTransformingSerializer to cut right into the data I want. In this example, I will assume I only care about the ID, so I can just create a generic Vehicle model.
#Serializable
data class VehicleResponse(
#Serializable(with = VehicleResponseSerializer::class)
#SerialName("data")
val vehicles: List<Vehicle>
)
#Serializable
data class Vehicle(val id: Int)
object VehicleResponseSerializer : JsonTransformingSerializer<List<Vehicle>>(ListSerializer(Vehicle.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
val vehicles = mutableListOf<JsonElement>()
// equals: [{"kind":"Car","data":{"id":1,"transmission":"manual"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]
val vehicleArray = element.jsonObject["children"]!!.jsonArray
vehicleArray.forEach { vehicle ->
// equals: {"id":1,"transmission":"manual"}}
val vehicleData = vehicle.jsonObject["data"]!!
vehicles.add(vehicleData)
}
return JsonArray(vehicles.toList())
}
}
The code works perfectly. Calling it out from main, printing the result gives me:
VehicleResponse(vehicles=[Vehicle(id=1), Vehicle(id=2)])
But they are actually Polymorphic!
Assuming one type does not work. I need to work with Car and Boat, and call their respective functions and properties.
I tried to model the structure like this:
#Serializable
data class VehicleResponse(
#Serializable(with = VehicleResponseSerializer::class)
#SerialName("data")
val vehicles: List<Vehicle>
)
#Serializable
abstract class Vehicle {
abstract val id: Int
}
#Serializable
data class Car(
override val id: Int,
val transmission: String,
) : Vehicle()
#Serializable
data class Boat(
override val id: Int,
val isDocked: Boolean,
) : Vehicle()
What I Want
I want to receive a JSON from a server, and instantly be able to deserialize it into a list of Vehicle objects, like the one VehicleResponse has.
I want to navigate through a deeply nested JSON, and unwrap an array that contains various Vehicle objects. For this, I assume I need to use JsonTransformingSerializer.
I want to use polymorphic deserialization to convert each of Vehicle into its corresponding subtype.
The actual ACTUAL problem
The thing that is truly throwing me in a loop is that a polymorphic serializer just does not seem to fit. It's called first before I get to parse the JSON. How am I supposed to decide which serializer to use?
Here's a test implementation:
object VehiclePolymorphicSerializer: JsonContentPolymorphicSerializer<VehicleResponse>(VehicleResponse::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out VehicleResponse> {
println("\nselectDeserializer()\n" +
"base element:\n" +
"$element\n")
// this return is a temporary hack, I just want to see the base element by printing it to the console
return VehicleResponse.serializer()
}
}
It prints:
selectDeserializer()
base element:
{"useless_info":"useless info","data":{"useless_info2":"useless info 2","children":[{"kind":"Car","data":{"id":1,"transmission":"manual"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]}}
That's the whole initial JSON! How am I supposed to decide which deserialization strategy to use, if both Car and Boat are in there? The JsonTransformingSerializer is called after the JsonContentPolymorphicSerializer.
Really not sure how am I supposed to proceed here. Would really appreciate even a slight hint.

kotlinx.serialization can handle polymorphic deserialization in this case without custom JsonContentPolymorphicSerializer. You just need to preserve class descriminator in the JSON returned from your JsonTransformingSerializer:
object VehicleResponseSerializer : JsonTransformingSerializer<List<Vehicle>>(ListSerializer(Vehicle.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
// equals: [{"kind":"Car","data":{"id":1,"totalWheels":"4"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]
val vehicleArray = element.jsonObject["children"]!!.jsonArray
// equals: [{"type":"Car","id":1,"totalWheels":"4"}, {"type":"Boat","id":2,"isDocked":true}]
return JsonArray(vehicleArray.map {
val data = it.jsonObject["data"]!!.jsonObject
val type = it.jsonObject["kind"]!!
JsonObject(
data.toMutableMap().apply { this["type"] = type }
/*
//Kotlin 1.4 offers a nicer way to do this:
buildMap {
putAll(data)
put("type", type)
}
*/
)
})
}
}
If you declare Vehicle class as sealed (not just abstract), you're already good to go. If you want to keep it abstract, then you need to register all its subclasses in serializersModule
val module = SerializersModule {
polymorphic(Vehicle::class) {
subclass(Car::class)
subclass(Boat::class)
}
}
and pass it to JSON configuration
val kotlinx = Json {
ignoreUnknownKeys = true
serializersModule = module
}
UPDATE
(Alternative approach with combination of JsonTransformingSerializer & JsonContentPolymorphicSerializer)
Actually, it's possible to combine these two serializers.
For the sake of justification for this approach, let's imagine that original JSON doesn't have that nice kind field, and we have to figure out the actual subtype of Vehicle by the shape of JSON. In this case it could be the following heuristic: "if there is a isDocked field, then it's a Boat, othrewise - a Car".
Yes, we may include this logic into JsonTransformingSerializer to create class descriminator on the fly:
val type = when {
"isDocked" in data -> JsonPrimitive("Boat")
else -> JsonPrimitive("Car")
}
But it's more common (and type-safe) to use JsonContentPolymorphicSerializer for this. So we may simplify JsonTransformingSerializer:
object VehicleResponseSerializer : JsonTransformingSerializer<List<Vehicle>>(ListSerializer(Vehicle.serializer())) {
override fun transformDeserialize(element: JsonElement): JsonElement {
// equals: [{"kind":"Car","data":{"id":1,"totalWheels":"4"}},{"kind":"Boat","data":{"id":2,"isDocked":true}}]
val vehicleArray = element.jsonObject["children"]!!.jsonArray
// equals: [{"id":1,"totalWheels":"4"}, {"id":2,"isDocked":true}] // Note that class discriminator is absent here!
return JsonArray(vehicleArray.map { it.jsonObject["data"]!! })
}
}
and define JsonContentPolymorphicSerializer (for Vehicle, not for VehicleResponse!) to handle actual serializer selection:
object VehiclePolymorphicSerializer : JsonContentPolymorphicSerializer<Vehicle>(Vehicle::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<out Vehicle> = when {
"isDocked" in element.jsonObject -> Boat.serializer()
else -> Car.serializer()
}
}
Since JsonTransformingSerializer (aka VehicleResponseSerializer) is registered for vehicles field serialization it's called before JsonContentPolymorphicSerializer (aka VehiclePolymorphicSerializer). Actually, the latter one is not yet called at all. We need to explicitly register it in serializersModule and pass it to JSON configuration (regardless of whether Vehicle class is declared as abstract or sealed):
val module = SerializersModule {
polymorphicDefault(Vehicle::class) { VehiclePolymorphicSerializer }
}
val kotlinx = Json {
ignoreUnknownKeys = true
serializersModule = module
}

Related

Kotlin Generic auto conversion to "out"

In the below code if we use generic in base and then extend it in a diff interface, kotlin doesn't respect the generic of the base interface.
Why is that so?
In the base I have not used "in" or "out" but still the extended interface by default becomes "out".
interface FeaturedCardAdapterContract {
interface View {
fun onCreate()
}
interface SubPresenter<V : View> {
fun onBind(v: V)
}
}
interface FeaturedTestAdapterContract {
interface View : FeaturedCardAdapterContract.View
interface Presenter : FeaturedCardAdapterContract.SubPresenter<View>
}
fun main() {
val featureImpl1: FeaturedTestAdapterContract.Presenter = object : FeaturedTestAdapterContract.Presenter {
override fun onBind(v: FeaturedTestAdapterContract.View) {
}
}
val featureImpl2: FeaturedTestAdapterContract.Presenter = object : FeaturedTestAdapterContract.Presenter {
override fun onBind(v: FeaturedTestAdapterContract.View) {
}
}
//Works but i won't be able to consume it in onBind bcz kotlin assumed it as "out"
val interfaceArray: Array<FeaturedCardAdapterContract.SubPresenter<out FeaturedCardAdapterContract.View>> = arrayOf(featureImpl1, featureImpl2)
//Dosen't Work-bcz kotlin assumes the type of featureImpl1 is FeaturedCardAdapterContract.SubPresenter<out FeaturedCardAdapterContract.View> ,Why?
val interfaceArray: Array<FeaturedCardAdapterContract.SubPresenter<FeaturedCardAdapterContract.View>> = arrayOf(featureImpl1, featureImpl2)
//Works but,Same as 1st method
val interfaceArray: Array<FeaturedCardAdapterContract.SubPresenter<*>> = arrayOf(featureImpl1, featureImpl2)
for (featureImpl in interfaceArray) {
//Won't work bcz of "out"
featureImpl.onBind(object : FeaturedCardAdapterContract.View {
override fun onCreate() {
//
}
})
}
}
Rename the interfaces to Processor, Animal, and Dog, and you will see why the compiler is correct about the types and what you are trying to do doesn't make sense.
Here's the renaming:
interface Animal // FeaturedCardAdapterContract.View
interface Processor<A: Animal> { // FeaturedCardAdapterContract.SubPresenter<V>
fun process(animal: A) // onBind
}
interface Dog: Animal // FeaturedTestAdapterContract.View
interface DogProcessor: Processor<Dog> // FeaturedTestAdapterContract.Presenter
In main, you are creating an array of 2 DogProcessors:
val processorImpl1 = object: DogProcessor {
override fun process(animal: Dog) {
}
}
val processorImpl2 = object: DogProcessor {
override fun process(animal: Dog) {
}
}
val array = arrayOf(processorImpl1, processorImpl2)
Then you are trying to loop through it and have them each process an animal:
val array = arrayOf(processorImpl1, processorImpl2)
for (processor in array) {
processor.process(object: Animal {
})
}
This is obviously not going to work no matter how you change the type of array. The processors in the array process dogs specifically, not animals in general. You could simply make this work by just giving it dogs instead of animals, or in your case:
val interfaceArray = arrayOf(featureImpl1, featureImpl2)
for (featureImpl in interfaceArray) {
featureImpl.onBind(object : FeaturedTestAdapterContract.View {
override fun onCreate() {
//
}
})
}
Note that the type of the array can be changed to Array<Processor<out Animal>> - an array of processors that only produces animals. This is because a producer of dogs is a kind of producer of animals. See also: PECS. However, since you want to call process (onBind) here, you want the processor to take in, or consume an animal, not produce one. Therefore, Array<Processor<out Animal>> is not what you want.
Just to clarify, you have defined featureImpl1 as FeaturedTestAdapterContract.Presenter, so it's a FeaturedCardAdapterContract.SubPresenter<FeaturedTestAdapterContract.View>.
Note the "Test" view here, not the "Card" one. This is your own definition of Presenter - the View you use in the definition is a shortcut for the test view FeaturedTestAdapterContract.View, NOT the card one FeaturedCardAdapterContract.View:
val featureImpl1: FeaturedTestAdapterContract.Presenter = object : FeaturedTestAdapterContract.Presenter {
// only wants test views here
override fun onBind(v: FeaturedTestAdapterContract.View) {
}
Now check this part:
Won't work bcz of "out"
featureImpl.onBind(object : FeaturedCardAdapterContract.View {
//...
})
Let's forget about out for the moment. You have defined your featureImpl1 so it accepts to bind only to the specific FeaturedTestAdapterContract.View. But here you're trying to pass a card view FeaturedCardAdapterContract.View, which is NOT a test view. If this were allowed, the body of featureImpl1 would just fail because it is given objects that are NOT of type FeaturedTestAdapterContract.View, nor even subtypes of it.
//Works but i won't be able to consume it in onBind bcz kotlin assumed it as "out"
val interfaceArray: Array<FeaturedCardAdapterContract.SubPresenter<out FeaturedCardAdapterContract.View>> = arrayOf(featureImpl1, featureImpl2)
Kotlin didn't assume anything here, you're marking out yourself. But it's normal that you have to write it because of what I explained above.
We've just seen that featureImpl1 is a SubPresenter<FeaturedTestAdapterContract.View>. It cannot be assigned to a SubPresenter<FeaturedCardAdapterContract.View> (without out) because that would mean it would need to accept more types than it actually can.

#JsonClassDiscriminator doesn't change json class discriminator

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)
}
}
}

How to parse JSON objects into an enum

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)

How to obtain all subclasses of a given sealed class?

Recently we upgraded one of our enum class to sealed class with objects as sub-classes so we can make another tier of abstraction to simplify code. However we can no longer get all possible subclasses through Enum.values() function, which is bad because we heavily rely on that functionality. Is there a way to retrieve such information with reflection or any other tool?
PS: Adding them to a array manually is unacceptable. There are currently 45 of them, and there are plans to add more.
This is how our sealed class looks like:
sealed class State
object StateA: State()
object StateB: State()
object StateC: State()
....// 42 more
If there is an values collection, it will be in this shape:
val VALUES = setOf(StateA, StateB, StateC, StateC, StateD, StateE,
StateF, StateG, StateH, StateI, StateJ, StateK, StateL, ......
Naturally no one wants to maintain such a monster.
In Kotlin 1.3+ you can use sealedSubclasses.
In prior versions, if you nest the subclasses in your base class then you can use nestedClasses:
Base::class.nestedClasses
If you nest other classes within your base class then you'll need to add filtering. e.g.:
Base::class.nestedClasses.filter { it.isFinal && it.isSubclassOf(Base::class) }
Note that this gives you the subclasses and not the instances of those subclasses (unlike Enum.values()).
With your particular example, if all of your nested classes in State are your object states then you can use the following to get all of the instances (like Enum.values()):
State::class.nestedClasses.map { it.objectInstance as State }
And if you want to get really fancy you can even extend Enum<E: Enum<E>> and create your own class hierarchy from it to your concrete objects using reflection. e.g.:
sealed class State(name: String, ordinal: Int) : Enum<State>(name, ordinal) {
companion object {
#JvmStatic private val map = State::class.nestedClasses
.filter { klass -> klass.isSubclassOf(State::class) }
.map { klass -> klass.objectInstance }
.filterIsInstance<State>()
.associateBy { value -> value.name }
#JvmStatic fun valueOf(value: String) = requireNotNull(map[value]) {
"No enum constant ${State::class.java.name}.$value"
}
#JvmStatic fun values() = map.values.toTypedArray()
}
abstract class VanillaState(name: String, ordinal: Int) : State(name, ordinal)
abstract class ChocolateState(name: String, ordinal: Int) : State(name, ordinal)
object StateA : VanillaState("StateA", 0)
object StateB : VanillaState("StateB", 1)
object StateC : ChocolateState("StateC", 2)
}
This makes it so that you can call the following just like with any other Enum:
State.valueOf("StateB")
State.values()
enumValueOf<State>("StateC")
enumValues<State>()
UPDATE
Extending Enum directly is no longer supported in Kotlin. See
Disallow to explicitly extend Enum class : KT-7773.
With Kotlin 1.3+ you can use reflection to list all sealed sub-classes without having to use nested classes: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-class/sealed-subclasses.html
I asked for some feature to achieve the same without reflection: https://discuss.kotlinlang.org/t/list-of-sealed-class-objects/10087
Full example:
sealed class State{
companion object {
fun find(state: State) =
State::class.sealedSubclasses
.map { it.objectInstance as State}
.firstOrNull { it == state }
.let {
when (it) {
null -> UNKNOWN
else -> it
}
}
}
object StateA: State()
object StateB: State()
object StateC: State()
object UNKNOWN: State()
}
A wise choice is using ServiceLoader in kotlin. and then write some providers to get a common class, enum, object or data class instance. for example:
val provides = ServiceLoader.load(YourSealedClassProvider.class).iterator();
val subInstances = providers.flatMap{it.get()};
fun YourSealedClassProvider.get():List<SealedClass>{/*todo*/};
the hierarchy as below:
Provider SealedClass
^ ^
| |
-------------- --------------
| | | |
EnumProvider ObjectProvider ObjectClass EnumClass
| |-------------------^ ^
| <uses> |
|-------------------------------------------|
<uses>
Another option, is more complicated, but it can meet your needs since sealed classes in the same package. let me tell you how to archive in this way:
get the URL of your sealed class, e.g: ClassLoader.getResource("com/xxx/app/YourSealedClass.class")
scan all jar entry/directory files in parent of sealed class URL, e.g: jar://**/com/xxx/app or file://**/com/xxx/app, and then find out all the "com/xxx/app/*.class" files/entries.
load filtered classes by using ClassLoader.loadClass(eachClassName)
check the loaded class whether is a subclass of your sealed class
decide how to get the subclass instance, e.g: Enum.values(), object.INSTANCE.
return all of instances of the founded sealed classes
If you want use it at child class try this.
open class BaseSealedClass(val value: String, val name: Int) {
companion object {
inline fun<reified T:BaseSealedClass> valueOf(value: String): T? {
return T::class.nestedClasses
.filter { clazz -> clazz.isSubclassOf(T::class) }
.map { clazz -> clazz.objectInstance }
.filterIsInstance<T>()
.associateBy { it.value }[value]
}
inline fun<reified T:BaseSealedClass> values():List<T> =
T::class.nestedClasses
.filter { clazz -> clazz.isSubclassOf(T::class) }
.map { clazz -> clazz.objectInstance }
.filterIsInstance<T>()
}
}
#Stable
sealed class Theme(value: String, name: Int): BaseSealedClass(value, name) {
object Auto: Theme(value = "auto", name = R.string.setting_general_theme_auto)
object Light: Theme(value= "light", name = R.string.setting_general_theme_light)
object Dark: Theme(value= "dark", name = R.string.setting_general_theme_dark)
companion object {
fun valueOf(value: String): Theme? = BaseSealedClass.valueOf(value)
fun values():List<Theme> = BaseSealedClass.values()
}
}
For a solution without reflection this is a library that supports generating a list of types to sealed classes at compile time:
https://github.com/livefront/sealed-enum
The example in the docs
sealed class Alpha {
object Beta : Alpha()
object Gamma : Alpha()
#GenSealedEnum
companion object
}
will generate the following object:
object AlphaSealedEnum : SealedEnum<Alpha> {
override val values: List<Alpha> = listOf(
Alpha.Beta,
Alpha.Gamma
)
override fun ordinalOf(obj: Alpha): Int = when (obj) {
Alpha.Beta -> 0
Alpha.Gamma -> 1
}
override fun nameOf(obj: AlphaSealedEnum): String = when (obj) {
Alpha.Beta -> "Alpha_Beta"
Alpha.Gamma -> "Alpha_Gamma"
}
override fun valueOf(name: String): AlphaSealedEnum = when (name) {
"Alpha_Beta" -> Alpha.Beta
"Alpha_Gamma" -> Alpha.Gamma
else -> throw IllegalArgumentException("""No sealed enum constant $name""")
}
}
The short version is
State::class.sealedSubclasses.mapNotNull { it.objectInstance }

How can I set the JsName for a property's backing field in Kotlin?

I played about with Kotlin's unsupported JavaScript backend in 1.0.x and am now trying to migrate my toy project to 1.1.x. It's the barest bones of a single-page web app interfacing with PouchDB. To add data to PouchDB you need JavaScript objects with specific properties _id and _rev. They also need to not have any other properties beginning with _ because they're reserved by PouchDB.
Now, if I create a class like this, I can send instances to PouchDB.
class PouchDoc(
var _id: String
) {
var _rev: String? = null
}
However, if I do anything to make the properties virtual -- have them override an interface, or make the class open and create a subclass which overrides them -- the _id field name becomes mangled to something like _id_mmz446$_0 and so PouchDB rejects the object. If I apply #JsName("_id") to the property, that only affects the generated getter and setter -- it still leaves the backing field with a mangled name.
Also, for any virtual properties whose names don't begin with _, PouchDB will accept the object but it only stores the backing fields with their mangled names, not the nicely-named properties.
For now I can work around things by making them not virtual, I think. But I was thinking of sharing interfaces between PouchDoc and non-PouchDoc classes in Kotlin, and it seems I can't do that.
Any idea how I could make this work, or does it need a Kotlin language change?
I think your problem should be covered by https://youtrack.jetbrains.com/issue/KT-8127
Also, I've created some other related issues:
https://youtrack.jetbrains.com/issue/KT-17682
https://youtrack.jetbrains.com/issue/KT-17683
And right now You can use one of next solutions, IMO third is most lightweight.
interface PouchDoc1 {
var id: String
var _id: String
get() = id
set(v) { id = v}
var rev: String?
var _rev: String?
get() = rev
set(v) { rev = v}
}
class Impl1 : PouchDoc1 {
override var id = "id0"
override var rev: String? = "rev0"
}
interface PouchDoc2 {
var id: String
get() = this.asDynamic()["_id"]
set(v) { this.asDynamic()["_id"] = v}
var rev: String?
get() = this.asDynamic()["_rev"]
set(v) { this.asDynamic()["_rev"] = v}
}
class Impl2 : PouchDoc2 {
init {
id = "id1"
rev = "rev1"
}
}
external interface PouchDoc3 { // marker interface
}
var PouchDoc3.id: String
get() = this.asDynamic()["_id"]
set(v) { this.asDynamic()["_id"] = v}
var PouchDoc3.rev: String?
get() = this.asDynamic()["_rev"]
set(v) { this.asDynamic()["_rev"] = v}
class Impl3 : PouchDoc3 {
init {
id = "id1"
rev = "rev1"
}
}
fun keys(a: Any) = js("Object").getOwnPropertyNames(a)
fun printKeys(a: Any) {
println(a::class.simpleName)
println(" instance keys: " + keys(a).toString())
println("__proto__ keys: " + keys(a.asDynamic().__proto__).toString())
println()
}
fun main(args: Array<String>) {
printKeys(Impl1())
printKeys(Impl2())
printKeys(Impl3())
}
I got a good answer from one of the JetBrains guys, Alexey Andreev, over on the JetBrains forum at https://discuss.kotlinlang.org/t/controlling-the-jsname-of-fields-for-pouchdb-interop/2531/. Before I describe that, I'll mention a further failed attempt at refining #bashor's answer.
Property delegates
I thought that #bashor's answer was crying out to use property delegates but I couldn't get that to work without infinite recursion.
class JSMapDelegate<T>(
val jsobject: dynamic
) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
return jsobject[property.name]
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
jsobject[property.name] = value
}
}
external interface PouchDoc4 {
var _id: String
var _rev: String
}
class Impl4() : PouchDoc4 {
override var _id: String by JSMapDelegate<String>(this)
override var _rev: String by JSMapDelegate<String>(this)
constructor(_id: String) : this() {
this._id = _id
}
}
The call within the delegate to jsobject[property.name] = value calls the set function for the property, which calls the delegate again ...
(Also, it turns out you can't put a delegate on a property in an interface, even though you can define a getter/setter pair which work just like a delegate, as #bashor's PouchDoc2 example shows.)
Using an external class
Alexey's answer on the Kotlin forums basically says, "You're mixing the business (with behaviour) and persistence (data only) layers: the right answer would be to explicitly serialise to/from JS but we don't provide that yet; as a workaround, use an external class." The point, I think, is that external classes don't turn into JavaScript which defines property getters/setters, because Kotlin doesn't let you define behaviour for external classes. Given that steer, I got the following to work, which does what I want.
external interface PouchDoc5 {
var _id: String
var _rev: String
}
external class Impl5 : PouchDoc5 {
override var _id: String
override var _rev: String
}
fun <T> create(): T = js("{ return {}; }")
fun Impl5(_id: String): Impl5 {
return create<Impl5>().apply {
this._id = _id
}
}
The output of keys for this is
null
instance keys: _id
__proto__ keys: toSource,toString,toLocaleString,valueOf,watch,unwatch,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,__defineGetter__,__defineSetter__,__lookupGetter__,__lookupSetter__,__proto__,constructor
Creating external classes
Three notes about creating instances of external classes. First, Alexey said to write
fun <T> create(): T = js("{}")
but for me (with Kotlin 1.1) that turns into
function jsobject() {
}
whose return value is undefined. I think this might be a bug, because the official doc recommends the shorter form, too.
Second, you can't do this
fun Impl5(_id: String): Impl5 {
return (js("{}") as Impl5).apply {
this._id = _id
}
}
because that explicitly inserts a type-check for Impl5, which throws ReferenceError: Impl5 is not defined (in Firefox, at least). The generic function approach skips the type-check. I'm guessing that's not a bug, since Alexey recommended it, but it seems odd, so I'll ask him.
Lastly, you can mark create as inline, though you'll need to suppress a warning :-)