kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

@Flat option annotation

Open InsanusMokrassar opened this issue 5 years ago • 9 comments

I offer to include @Flat (or @Flatten, for example) annotation which will be used in cases when value object of field must be included directly into parent object.

@Serializable
data class Parent(
    val stringField: String = "stringValue",
    @Flat
    val flatField: Child = Child()
)

@Serializable
class Child {
    val childString: String = "childValue"
}

fun main(args: Array<String>) {
    println(JSON.stringify(Parent.serializer(), Parent()))
}

Will give result:

{"stringField":"stringValue","childString":"childValue"}

Here I see a few problems:

  • String and other primitives must be resolved like common fields (with their keys) or those fields must lead to exception like IllegalArgumentException
  • Collections in this case, I think, must be interpreted as associated map: with keys numbers and values - values if list

InsanusMokrassar avatar Dec 01 '18 03:12 InsanusMokrassar

So, next code will give the same effect:

@Serializable(ParentFlatChildSerializer::class)
data class Parent(
    val stringField: String = "stringValue",
    val flatField: Child = Child()
)

@Serializable
class Child {
    @Optional
    val flatFieldValue: String = "flatFieldValue"
}

@Serializer(Parent::class)
object ParentFlatChildSerializer : KSerializer<Parent> {
    override fun serialize(output: Encoder, obj: Parent) {
        output.beginStructure(
            descriptor
        ).apply {
            encodeStringElement(
                descriptor, 0, obj.stringField
            )
            encodeStringElement(Child.serializer().descriptor, 0, obj.flatField.flatFieldValue)
        }.endStructure(
            descriptor
        )
    }
}

fun main(args: Array<String>) {
    println(JSON.stringify(ParentFlatChildSerializer, Parent()))
}

But it is much longer and is not useful to write serializer like this for each object/class in projects.

InsanusMokrassar avatar Dec 01 '18 03:12 InsanusMokrassar

Any information about this? Is there some workaround, or will it be implemented in near future?

Coneys avatar Dec 28 '20 00:12 Coneys

This would be nice to have.

Elvis10ten avatar May 01 '22 11:05 Elvis10ten

I join the queue to request this feature. It would be extremely useful indeed. Thank you!

marcosalis avatar May 18 '22 08:05 marcosalis

Same here

micHar avatar May 18 '22 13:05 micHar

There are two workarounds. One is inline classes (or tricking the serialization framework into thinking the class is inline - using a custom SerialDescriptor). The other is to use a custom serializer. If you give its descriptor as PrimitiveKind.String (there is a factory function for primitive descriptors, not even "internal"), then you can have the implementation just use encodeString, decodeString and do the "right" translation to get the objects.

What isn't really possible is to embed multi-attribute members into a parent (neither way really works), and that would require quite some changes on the compiler plugin side.

pdvrieze avatar May 18 '22 15:05 pdvrieze

Rusts serde has a great impl of this, could be of use when designing the API if its pursued

CharlieTap avatar Apr 23 '23 20:04 CharlieTap

Not the best solution, but a bit better than writing a full serializer. Uses JsonTransformingSerializer:

import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonTransformingSerializer
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.jsonObject

@Serializable
data class Parent(
    val stringField: String = "stringValue",
    val flatField: Child = Child()
)

@Serializable
class Child {
    val childString: String = "childValue"
    val otherChildString: String = "otherChildVale"
}

object UnwrappingJsonSerializer: JsonTransformingSerializer<Parent>(Parent.serializer()) {
    override fun transformSerialize(element: JsonElement) = buildJsonObject {
        element.jsonObject.forEach { (propertyName, propertyValue) ->
            if (propertyName == Parent::flatField.name) {
                propertyValue.jsonObject.forEach(::put)
            } else {
                put(propertyName, propertyValue)
            }
        }
    }
}

fun main(args: Array<String>) {
    println(
        Json {
            prettyPrint = true
            encodeDefaults = true
        }.encodeToString(
            serializer = UnwrappingJsonSerializer,
            value = Parent()
        )
    )
}

will print:

{
    "stringField": "stringValue",
    "childString": "childValue",
    "otherChildString": "otherChildVale"
}

monosoul avatar Jun 08 '23 17:06 monosoul