Kotlinx.serialization: Inline serializer

Created on 25 Jan 2019  Â·  8Comments  Â·  Source: Kotlin/kotlinx.serialization

Any class with a single, not null, serializable field can be serialized as the containing field.

For example the class

data class CustomerId(val value: Long)

can be serialized

CustomerId(123)  →  123

This behaviour can be useful for some use case.

feature

Most helpful comment

If you don't want to build a stringly-typed application or don't want want to use brittle shallow models, this is an absolute must.

Say you have an online shop. If the price of an item is an Int, say in USD for argument's sake. Then never will -2 billion USD to +2 billion USD be the actual range that would make sense within your domain context. A more robust application therefore needs to wrap this Int into a data class that comes with the necessary range restrictions (say must be between 1 and 9999).

Currently the effort to add serialization is utterly disproportionate to the effort it takes to create the class itself.

Where the class itself simply boils down to

data class Price(val price: Int) {
    init {
        require(price > 0 && price < 10000) { "Invalid price" }
    }
}

Once you add basic serialization it balloons to

@Serializable
data class Price(val price: Int) {
    init {
        require(price > 0 && price < 10000) { "Invalid price" }
    }

    @Serializer(forClass = Price::class)
    companion object : KSerializer<Price> {
        override val descriptor = PrimitiveDescriptor(Price::class.simpleName!!, PrimitiveKind.INT)
        override fun serialize(encoder: Encoder, value: Price) = encoder.encodeInt(value.price)
        override fun deserialize(decoder: Decoder) = Price(decoder.decodeInt())
    }
}

Clearly there must be a better way!

This should for example be simplified to something like this

@Serializable(inline = true)
data class Price(val price: Int) {
    init {
        require(price > 0 && price < 10000) { "Invalid price" }
    }
}

All 8 comments

I think it is not a very common use-case, and can be easily implemented manually (https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/custom_serializers.md#representing-classes-as-a-single-value), so I doubt that special support for this would be added to the compiler plugin.

I think it is not a very common use-case

I adopted a solution pretty similar to https://jakewharton.com/inline-classes-make-great-database-ids/ and I use "single field" class to describe different unit of measures, similar to https://github.com/kunalsheth/units-of-measure/blob/master/demo/src/main/kotlin/info/kunalsheth/units/sample/Sample.kt

can be easily implemented manually

I did it, yet.
But I would like to not do it, anymore.
(Incidentally: your statement can be used against data class feature).

I also have plenty of single-property classes which are supposed to serialize to that one property.
Most of them only exist though because I cannot mark these classes inline because the library won't support them yet.
So I guess there's no way around writing dozens of Serializers for them for now?

I also would like to have this.
How about @Transparent or @Inline annotation on such classes?

If you don't want to build a stringly-typed application or don't want want to use brittle shallow models, this is an absolute must.

Say you have an online shop. If the price of an item is an Int, say in USD for argument's sake. Then never will -2 billion USD to +2 billion USD be the actual range that would make sense within your domain context. A more robust application therefore needs to wrap this Int into a data class that comes with the necessary range restrictions (say must be between 1 and 9999).

Currently the effort to add serialization is utterly disproportionate to the effort it takes to create the class itself.

Where the class itself simply boils down to

data class Price(val price: Int) {
    init {
        require(price > 0 && price < 10000) { "Invalid price" }
    }
}

Once you add basic serialization it balloons to

@Serializable
data class Price(val price: Int) {
    init {
        require(price > 0 && price < 10000) { "Invalid price" }
    }

    @Serializer(forClass = Price::class)
    companion object : KSerializer<Price> {
        override val descriptor = PrimitiveDescriptor(Price::class.simpleName!!, PrimitiveKind.INT)
        override fun serialize(encoder: Encoder, value: Price) = encoder.encodeInt(value.price)
        override fun deserialize(decoder: Decoder) = Price(decoder.decodeInt())
    }
}

Clearly there must be a better way!

This should for example be simplified to something like this

@Serializable(inline = true)
data class Price(val price: Int) {
    init {
        require(price > 0 && price < 10000) { "Invalid price" }
    }
}

Similar: #292

I think it is not a very common use-case, and can be easily implemented manually

Definitely not so easy when you have to do it a lot. Also, I would argue that if it's not a common use-case it's only because it's not easy. It's a useful pattern. #292 would probably be even more useful though in a lot of cases - perhaps most cases.

292 injects fields in the parent object and it encodes objects differently.

Example:

@InlineSerializer
data class CustomerId(val value: Long)

data class Customer(val id: CustomerId, val name: String)

292 serializes a Customer

{ "value": 3, "name": "Joe" }

instead this proposal

{ "id": 3, "name": "Joe" }
Was this page helpful?
0 / 5 - 0 ratings

Related issues

daweedm picture daweedm  Â·  4Comments

slomkowski picture slomkowski  Â·  4Comments

altavir picture altavir  Â·  4Comments

sandwwraith picture sandwwraith  Â·  4Comments

lonevetad picture lonevetad  Â·  3Comments