Я пытаюсь закодировать запечатанный класс
json.encodeToString(
Filter.NOT(
Filter.Field(
name = "foo", value = "bar"
)
)
)
и вывести что-то совершенно другое, например
{
"not": {
"foo": "bar"
}
}
чтобы точно соответствовать ожидаемому JSON API, в который я интегрируюсь.
Структура данных представляет собой запечатанный интерфейс (с и, или, xor и т. д., но удаленный для простоты):
@Serializable
sealed interface Filter {
...
@Serializable
@SerialName("not")
class NOT(val not: Filter): Filter
@Serializable(with = Field.Companion::class)
data class Field(
val name: String,
val value: String
) : Filter {
companion object : KSerializer<Field> {
override val descriptor: SerialDescriptor
get() = mapSerialDescriptor<String, String>()
override fun serialize(encoder: Encoder, value: Field) {
println(value)
encoder.encodeStructure(mapSerialDescriptor<String, String>()) {
mutableMapOf("a" to "b", value.name to value.value)
}
}
override fun deserialize(decoder: Decoder): Field = TODO()
}
}
Если я кодируюToString, я получаю:
{
"not": {
"type": "kotlin.collections.HashMap"
}
}
если я удалю пользовательский сериализатор
{
"not": {
"type": "com.jvaas.testing.model.filter.Filter.Field",
"name": "foo",
"value": "bar"
}
}
Таким образом, типа там не должно быть, а имя + значение должно быть закодировано «имя»: «значение» вместо «имя»: имя, «значение»: значение для достижения
{
"not": {
"foo": "bar"
}
}
Пытаюсь с
@Serializable(with = Field.Companion::class)
data class Field(
val name: String,
val value: String
) : Filter {
companion object : JsonTransformingSerializer<Field>(Field.serializer()) {
override fun transformSerialize(element: JsonElement): JsonElement {
return JsonObject(mutableMapOf<String, JsonElement>().apply {
put("foo", JsonPrimitive("bar"))
})
}
}
}
я получаю
Вызвано: java.lang.NullPointerException: невозможно вызвать «com.jvaas.testing.model.filter.Filter$Field$Companion.serializer()», потому что «com.jvaas.testing.model.filter.Filter$Field.Companion» является нулевым
Есть идеи, как это можно сделать?
Обновлено: Пример JSON добавлен по запросу.
{
"filter": {
"or": [
{
"and": [
{
"field1_id": "1"
},
{
"field2_id": "A"
}
]
},
{
"nand": [
{
"field3_id": "2"
},
{
"field4_id": "B"
}
]
},
{
"xor": [
{
"field5_id": "3"
},
{
"field6_id": "C"
}
]
},
{
"nor": [
{
"field7_id": "D"
},
{
"not": {
"field8": [
"EF",
"GHI",
"JK"
],
"operator": "in"
}
}
]
}
]
}
}
Моделирование части and/or/nor/nand/xor/xnor/not — это простая часть, которая просто работает.
@Serializable
sealed interface Filter {
@Serializable
@SerialName("and")
class AND(val and: List<Filter>) : Filter {
constructor(vararg and: Filter) : this(and.toList())
}
@Serializable
@SerialName("or")
class OR(val or: List<Filter>) : Filter {
constructor(vararg or: Filter) : this(or.toList())
}
...
@Serializable(with = FieldSerializer::class)
data class Field(
val name: String,
val value: String
) : Filter {
...
}
}
также довольно интуитивно понятен в работе
json.encodeToString(
Filter.NOT(
Filter.AND(
Filter.Field(
name = "foo", value = "bar"
),
Filter.OR(
Filter.Field(
name = "stack", value = "overflow"
),
Filter.Field(
name = "rab", value = "oof"
),
)
)
)
)
Однако часть поля должна иметь возможность принимать любое динамическое имя (скажем, для аргумента field[a-zA-Z0-9_]*) плюс значение и вывод.
{
'field_that_can_be_anything_unknown_at_compile_time': 'some value'
}
В примере JSON вы заметите, что поле может иметь либо одно значение, либо список значений. В этом вопросе StackOverflow рассматривайте только поле с одним значением как часть вопроса. Я могу легко создать другой тип поля, называемый FieldIn, для обработки списка значений и решить его с помощью того же решения, которое используется для поля с одним значением.
готово, я добавил еще немного контекста, пример JSON и то, как выглядят классы AND / OR и т. д.
По умолчанию kotlinx.serialization
использует поле с именем «тип», чтобы различать доступные параметры в изолированной иерархии интерфейса/класса.
Если вы не хотите использовать этот механизм, вы можете предоставить собственную логику десериализации и избавиться от поля «тип».
Вы можете объединить это с настройкой KSerializer
для класса Filter.Field
.
Следующее должно достичь того, к чему вы стремились.
Я изменил иерархию типов следующим образом:
@Serializable(with = FilterSerializer::class)
sealed interface Filter {
@Serializable
class AND(val and: List<Filter>) : Filter {
constructor(vararg and: Filter) : this(and.toList())
}
@Serializable
class NOT(val not: List<Filter>) : Filter {
constructor(vararg and: Filter) : this(and.toList())
}
@Serializable
class OR(val or: List<Filter>) : Filter {
constructor(vararg or: Filter) : this(or.toList())
}
@Serializable(with = FieldSerializer::class)
data class Field(
val name: String,
val value: String
) : Filter
}
KSerializer
выглядят следующим образом:
object FieldSerializer : KSerializer<Filter.Field> {
override val descriptor: SerialDescriptor
get() = mapSerialDescriptor<String, String>()
override fun deserialize(decoder: Decoder): Filter.Field {
return decoder.decodeStructure(descriptor) {
val name = decodeStringElement(descriptor, 0)
val value = decodeStringElement(descriptor, 1)
Filter.Field(name, value)
}
}
override fun serialize(encoder: Encoder, value: Filter.Field) {
encoder.encodeStructure(descriptor) {
encodeStringElement(descriptor, 0, value.name)
encodeStringElement(descriptor, 1, value.value)
}
}
}
Это кодирует ваш Filter.Field
как простую пару ключ-значение. Я уверен, что есть и другие способы добиться этого, но этот работает.
Чтобы полиморфизм работал без поля «тип», вы можете предоставить свой собственный JsonContentPolymorphicSerializer
, который различает разные поля на основе ключей, присутствующих в объекте JSON.
object FilterSerializer : JsonContentPolymorphicSerializer<Filter>(Filter::class) {
override fun selectDeserializer(element: JsonElement): DeserializationStrategy<Filter> {
val key = element.jsonObject.keys.single()
return when(key) {
"and" -> Filter.AND.serializer()
"not" -> Filter.NOT.serializer()
"or" -> Filter.OR.serializer()
else -> Filter.Field.serializer()
}
}
}
Вы можете попробовать это на этом примере:
fun main() {
val json = Json {
prettyPrint = true
}
val value: Filter = Filter.NOT(
Filter.AND(
Filter.Field(
name = "foo", value = "bar"
),
Filter.OR(
Filter.Field(
name = "stack", value = "overflow"
),
Filter.Field(
name = "rab", value = "oof"
),
)
)
)
val encoded: String = json.encodeToString(value)
println(encoded)
val decoded: Filter = json.decodeFromString<Filter>(encoded)
println(decoded)
}
это красиво! спасибо, что нашли время, чтобы создать этот замечательный ответ
Интересный вопрос. Не могли бы вы предоставить пример JSON, включающий выражение с двумя параметрами, например
or
илиand
, чтобы лучше понять общую структуру, которую вы стремитесь достичь?