I'm using kotlinx.serialization
in a KMP sdk for response parsing. I've seen that the properties
module can be used to easily create the query map from a given class (I'm using interface inheritance to produce cleaner and safer code).
An example of a query class could be:
@Serializable
class MyQuery(
val latitude: QueryCollection<Float>, // alias to `@Serializable(with = ...) Collection<T>`
val variables: QueryCollection<Variables>,
)
I need to generate a ?latitude=1.0,2.0,-3.0&variables=hello,stack,overflow
string from it.
I got everything, but the deserialize
method working. It's not crucial to get it implemented, but I wanted to get a better understanding of the library. Unfortunately, I couldn't get the single element decoding step from String
to T
working.
Here's the trivial code:
class QueryCollectionSerializer<T>(
private val dataSerializer: KSerializer<T>
) : KSerializer<Collection<T>> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("QueryCollection<T>", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): Collection<T> =
TODO("Deserialization not (yet) implemented")
override fun serialize(encoder: Encoder, value: Collection<T>) =
encoder.encodeString(value.joinToString(","))
}
typealias QueryCollection<T> = @Serializable(QueryCollectionSerializer::class) Collection<T>
Point 1: Is the (string) primitive kind appropriate for this situation? Should I be using a ListSerializer instead?
Point 2: I tried
decoder.decodeString().split(',').map { /* magic String -> T... How? Idk */ }
decoder.decodeStructure
. I was only able to type/semantically correct code, but it wasn't decoding any element, since I didn't know how to tell the decoder that it should get a new element after a commaAm I missing something?
I suspect I could be forced to write a custom "Query" format by mostly copying the Properties one, but it honestly seems a bit too much for a feature that I barely anyone would be using (again, this is mostly a thing I want to understand, not just "solve for the sake of the sdk")
deserialize
I do not believe there is any "legitimate" way to implement deserialize
given how serialize
has been implemented. The dataSerializer
has been completely bypassed due to converting the collection directly into a string. That means nothing actually knows the "structure" of the collection's elements. Even if you can reliably split the string by a delimiter, you have no way of knowing how to convert each individual string to T
, let alone knowing what T
is in the first place.
Point 1: Is the (string) primitive kind appropriate for this situation? Should I be using a ListSerializer instead?
Since you've implemented serialize
to encode a String
, using a PrimitiveSerialDescriptor
with PrimitiveKind.STRING
is correct.
You are effectively trying to force a particular serial format via a KSerializer
. But the format is not the serializer's responsibility. In general, the "flow" of the kotlinx.serialization
library looks like:
+---------------+ +--------------------------+ +---------+ +---------------+
| | | | | | | |
| Kotlin Object |-->| SerializationStrategy* |-->| Encoder |-->| Serial Format |
| | | | | | | |
+---------------+ +--------------------------+ +---------+ +---------------+
+---------------+ +--------------------------+ +---------+ +---------------+
| | | | | | | |
| Kotlin Object |<--| DeserializationStrategy* |<--| Decoder |<--| Serial Format |
| | | | | | | |
+---------------+ +--------------------------+ +---------+ +---------------+
* KSerializer extends both SerializationStrategy and DeserializationStrategy
Interface | Purpose |
---|---|
SerializationStrategy |
Convert an object into primitives to pass to an Encoder . |
Encoder |
Format primitives into a serial format. |
Decoder |
Parse a serial format into primitives. |
DeserializationStrategy |
Convert primitives from a Decoder into an object. |
KSerializer |
Combination of both the XXXStrategy interfaces. Whenever you are creating a custom serializer, you will probably want to implement this interface (necessary if you want to specify your implementation via annotations). |
Note: A serializer can be composed of other serializers. This allows "nested" complex types to be serialized into primitives (e.g., String
, Int
, Boolean
, etc.), and vice versa, without the "parent" serializer actually knowing how. For example, the dataSerializer
in your serializer is what knows how to serialize and deserialize each T
.
As you can see, the Encoder
and Decoder
are responsible for the serial format. And ideally, a KSerializer
should be able to work with any implementation of Encoder
and Decoder
.
If you want to implement serialization to a certain serial format, and there is no existing library for that format, then you will have to implement your own Encoder
and/or Decoder
. The Kotlin Serialization Guide has a section on creating custom serial formats which may help, if you decide the effort is worth it. Though note custom formats are apparently still experimental.