I'm trying to encode a sealed class
json.encodeToString(
Filter.NOT(
Filter.Field(
name = "foo", value = "bar"
)
)
)
and output something completely different like
{
"not": {
"foo": "bar"
}
}
to exactly match the expected JSON of an API I'm integrating into.
The data structure is a sealed interface (with and, or, xor, etc, but removed for simplicity):
@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()
}
}
If I encodeToString, I currently get:
{
"not": {
"type": "kotlin.collections.HashMap"
}
}
if I remove the custom serializer
{
"not": {
"type": "com.jvaas.testing.model.filter.Filter.Field",
"name": "foo",
"value": "bar"
}
}
So the type should not be there, and name + value should be encoded "name": "value" instead of "name": name, "value": value to achieve
{
"not": {
"foo": "bar"
}
}
Trying with
@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"))
})
}
}
}
I'm getting
Caused by: java.lang.NullPointerException: Cannot invoke "com.jvaas.testing.model.filter.Filter$Field$Companion.serializer()" because "com.jvaas.testing.model.filter.Filter$Field.Companion" is null
Any idea how this can be done?
EDIT: Example JSON added as requested
{
"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"
}
}
]
}
]
}
}
Modelling the and / or / nor / nand / xor / xnor / not part is the easy part, that just works.
@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 {
...
}
}
also quite intuitive to work with too
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"
),
)
)
)
)
The field part however needs to be able to take any dynamic name (let's say for argument sake field[a-zA-Z0-9_]*) plus a value and output
{
'field_that_can_be_anything_unknown_at_compile_time': 'some value'
}
You'll notice in the example JSON that a field can either have a single value or a list of values. For this StackOverflow question, only consider the single value field as part of the question, I can easily create another field type called FieldIn to handle the list of values and solve it with the same solution that is used for a Field with a single value.
By default, kotlinx.serialization
uses a field named "type" to discriminate between the available options in a sealed interface / class hierarchy.
If you don't want to use this mechanism, you can provide your own logic for deserialisation and get rid of the "type" field.
You can combine this with a custom KSerializer
for the Filter.Field
class.
The following should achieve what you've aimed for.
I've adjusted the type hierarchy to the following:
@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
}
The KSerializer
s look as follows:
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)
}
}
}
This encodes your Filter.Field
as simple key-value pair. I'm certain there are other ways to achieve this but this works.
In order for the polymorphism to work without the "type" field, you can provide your own JsonContentPolymorphicSerializer
that discriminates the different fields based on the keys present in the JSON object.
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()
}
}
}
You can try it out with this example:
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)
}