Kotlin Сериализуемый Любой элемент

Я столкнулся с проблемой сериализации данных. 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
            )
        }
    }
}
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
80
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вероятно, самый простой способ — вообще не использовать 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 во время выполнения. Вместо этого вы можете использовать правильный тип, который уже доступен во время компиляции.

Мне кажется, нужно заменить DeserializationStrategy<State> на KSerializer<out State>. Или я ошибаюсь?

pie 26.05.2024 17:43

Мы перезаписываем функцию protected abstract fun selectDeserializer(element: JsonElement): DeserializationStrategy<T> (обратите внимание на тип возвращаемого значения). KSerializer является более общим, поскольку он также реализует SerializationStrategy (который нам здесь не нужен). Оба варианта подойдут, но DeserializationStrategy точнее.

Leviathan 26.05.2024 18:13
Ответ принят как подходящий

Это поле 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
    }

Это лучший ответ, чем мой, и он должен быть принят, он гораздо более краток.

Leviathan 26.05.2024 21:40

Интересный. Я думаю, что этот метод не сработает, если, например, для разных значений экземпляра я буду получать разные типы параметра значения. В этом случае, я думаю, будет большое количество дублирования кода. "rgb", "temperature_k" -> State.SomeInt.serializer()

pie 29.05.2024 16:16

Тип (запечатанного) класса позволяет вам определить фактическое значение значения, поскольку фактического поля «экземпляра» нет. Возможно, есть некоторые накладные расходы, но сопоставление конечных точек API с кодом почти всегда является болезненным.

Pawel 29.05.2024 16:44

Другие вопросы по теме