Я столкнулся с проблемой сериализации данных. REST API не принадлежит мне, и я не могу его изменить. По определенному запросу я получаю Объект:
data class State(
val instance: String,
val value: Any
)
В качестве параметра значения объекта могут выступать различные типы данных, например Boolean, String, Int, Float и даже Object, например:
"state": {
"instance": "rgb",
"value": 13910520
}
// OR
"state": {
"instance": "hsv",
"value": {
"h": 255,
"s": 100,
"v": 50
}
}
В этом случае тип значения зависит от экземпляра (экземпляр всегда имеет тип String). С помощью этого ответа я более-менее смог разобраться в различных типах, но не понимал, как мне следует действовать, если в ответ я получу Объект.
@Serializable(with = StateSerializer::class)
data class StateObject(
val instance: String,
val value: Any
)
object StateSerializer : KSerializer<StateObject> {
private val dataTypeSerializers: Map<String, KSerializer<Any>> =
mapOf(
"..." to serialDescriptor<Boolean>(),
"..." to serialDescriptor<String>(),
"..." to serialDescriptor<Int>()
).mapValues { (_, v) -> v as KSerializer<Any> }
private fun getValueSerializer(instance: String): KSerializer<Any> =
dataTypeSerializers[instance] ?: throw SerializationException()
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("StateObject") {
element("instance", serialDescriptor<String>())
element("value", buildClassSerialDescriptor("Any"))
}
override fun deserialize(decoder: Decoder): StateObject = decoder.decodeStructure(
descriptor) {
if (decodeSequentially()) {
val instance = decodeStringElement(descriptor, 0)
val value = decodeSerializableElement(
descriptor,
1,
getValueSerializer(instance)
)
StateObject(instance, value)
} else {
require(decodeElementIndex(descriptor) == 0) { }
val instance = decodeStringElement(descriptor, 0)
val value = when (val index = decodeElementIndex(descriptor)) {
1 -> decodeSerializableElement(descriptor, 1, getValueSerializer(instance))
CompositeDecoder.DECODE_DONE -> throw SerializationException("value field is missing")
else -> error("Unexpected index: $index")
}
StateObject(instance, value)
}
}
override fun serialize(encoder: Encoder, value: StateObject) {
encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.instance)
encodeSerializableElement(
descriptor,
1,
getValueSerializer(value.instance),
value.value
)
}
}
}





Вероятно, самый простой способ — вообще не использовать Any. Возможно, вы не сможете изменить REST API, но вы можете изменить свой класс данных. Вместо использования только одного класса данных со значением Any вам следует создать отдельные классы данных для каждого конкретного типа экземпляра:
sealed interface State {
val instance: String
val value: Any
@Serializable
data class RGB(
override val instance: String,
override val value: Int,
) : State
@Serializable
data class HSV(
override val instance: String,
override val value: HSVValues,
) : State
}
Если HSVValues это:
@Serializable
data class HSVValues(
val h: Int,
val s: Int,
val v: Int,
)
Просто добавьте оставшиеся случаи в интерфейс. Строковое состояние и логическое состояние могут выглядеть так:
@Serializable
data class SomeString(
override val instance: String,
override val value: String,
) : State
@Serializable
data class SomeBoolean(
override val instance: String,
override val value: Boolean,
) : State
Теперь вы можете использовать простой полиморфный сериализатор, например:
object StateSerializer : JsonContentPolymorphicSerializer<State>(State::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<State> {
return when (element.jsonObject["instance"]?.jsonPrimitive?.content) {
"rgb" -> State.RGB.serializer()
"hsv" -> State.HSV.serializer()
"..." -> State.SomeString.serializer()
"..." -> State.SomeBoolean.serializer()
else -> throw SerializationException("Unknown instance ${element.jsonObject["instance"]}")
}
}
}
Поскольку типы теперь четко определены, вам нужна только часть десериализации, сериализация работает «из коробки».
Затем аннотируйте интерфейс с помощью сериализатора, и все готово:
@Serializable(with = StateSerializer::class)
sealed interface State {
...
}
Использование выделенных классов данных для каждого типа экземпляров не только упрощает (де)сериализацию, но и облегчает работу с оставшимся кодом, поскольку вам не нужно обрабатывать какой-то неизвестный тип Any во время выполнения. Вместо этого вы можете использовать правильный тип, который уже доступен во время компиляции.
Мы перезаписываем функцию protected abstract fun selectDeserializer(element: JsonElement): DeserializationStrategy<T> (обратите внимание на тип возвращаемого значения). KSerializer является более общим, поскольку он также реализует SerializationStrategy (который нам здесь не нужен). Оба варианта подойдут, но DeserializationStrategy точнее.
Это поле instance выглядит как дискриминатор классов .
В этом случае вы можете использовать его для встроенной полиморфной обработки без необходимости написания собственного сериализатора:
@OptIn(ExperimentalSerializationApi::class)
@JsonClassDiscriminator("instance")
@Serializable
sealed class State {
abstract val value: Any
@Serializable
@SerialName("rgb")
data class RGB(override val value: Int) : State()
@Serializable
@SerialName("hsv")
data class HSV(override val value: HSValues) : State()
// declare all possible types
}
Это лучший ответ, чем мой, и он должен быть принят, он гораздо более краток.
Интересный. Я думаю, что этот метод не сработает, если, например, для разных значений экземпляра я буду получать разные типы параметра значения. В этом случае, я думаю, будет большое количество дублирования кода. "rgb", "temperature_k" -> State.SomeInt.serializer()
Тип (запечатанного) класса позволяет вам определить фактическое значение значения, поскольку фактического поля «экземпляра» нет. Возможно, есть некоторые накладные расходы, но сопоставление конечных точек API с кодом почти всегда является болезненным.
Мне кажется, нужно заменить
DeserializationStrategy<State>наKSerializer<out State>. Или я ошибаюсь?