j

    Joost Klitsie

    11 months ago
    Hi all! I have sort of my own take on the Kotlin Result class and I wish to serialize it. However, I fail to achieve this... Does someone know how to serialize the code I will attach to the thread? 🙂 Also, if someone already made the kotlin result class serializable, I would be interested in how that works 🙂
    sealed interface Result<T> {
    
    	val value: T?
    
    }
    
    fun <T> success(value: T): Success<T> = Success(value)
    
    fun <T> failure(error: CommonException): Failure<T> = Failure(error)
    
    fun <T> loading(): Loading<T> = Loading<T>()
    
    @JvmInline
    value class Success<T> constructor(private val _value: Any?) : Result<T> {
    
    	override val value: T
    		get() = _value as T
    
    }
    
    @JvmInline
    value class Failure<T> constructor(
    	val error: CommonException
    ) : Result<T> {
    
    	override val value: T?
    		get() = null
    
    }
    
    class Loading<T> : Result<T> {
    
    	override val value: T?
    		get() = null
    
    	override fun equals(other: Any?): Boolean = other is Loading<*>
    
    	override fun hashCode(): Int = 1
    
    	override fun toString(): String = "Loading"
    
    }
    inspired by https://github.com/JetBrains/kotlin/blob/master/libraries/stdlib/src/kotlin/util/Result.kt
    The
    <T>
    will always be a serializable class, but it is not sealed.
    e

    ephemient

    11 months ago
    using
    @JvmInline
    like that is rather pointless, since it's only accessible via the public interface which will box it…
    j

    Joost Klitsie

    11 months ago
    Perhaps I will get rid of the Success, Loading and Failure interface and directly implement Result<T> instead with public implementations
    but then I am still left with my failure to serialize the bunch 🙂
    I updated the snippet
    e

    ephemient

    11 months ago
    if you just used a sealed class and made some minor adjustments, then everything would work right out of the box:
    @Serializable
    sealed class Result<out T> {
        abstract val value: T?
    }
    
    @Serializable
    class Success<T> internal constructor(override val value: T) : Result<T>()
    
    @Serializable
    class Failure internal constructor(val error: CommonException) : Result<Nothing>() {
        override val value: Nothing?
            get() = null
    }
    (assuming CommonException is serializable) this will result in an auto-generated
    fun <T> Result.Companion.serializer(tSerializer: KSerializer<T>): KSerializer<Result<T>>
    if you really a
    sealed interface
    for whatever reason, then sure, you could replicate by hand what the plugin would generate in the other case:
    @Serializable(with = ResultSerializer::class)
    sealed interface Result<out T> {
        val value: T?
    }
    
    class ResultSerializer<T>(private val tSerializer: KSerializer<T>) : KSerializer<Result<T>> {
        override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Result", tSerializer.descriptor) {
            element("value", tSerializer.descriptor)
            element("error", CommonException.serializer().descriptor)
        }
    
        override fun serialize(encoder: Encoder, value: Result<T>) {
            encoder.encodeStructure(descriptor) {
                when (value) {
                    is Success -> encodeSerializableElement(descriptor, 0, tSerializer, value.value)
                    is Failure -> encodeSerializableElement(descriptor, 1, CommonException.serializer(), value.error)
                }
            }
        }
    
        override fun deserialize(decoder: Decoder): Result<T> {
            var result: Result<T>? = null
            decoder.decodeStructure(descriptor) {
                while (true) {
                    when (val index = decodeElementIndex(descriptor)) {
                        0 -> result = success(decodeSerializableElement(descriptor, 0, tSerializer))
                        1 -> result = failure(decodeSerializableElement(descriptor, 1, CommonException.serializer()))
                        CompositeDecoder.DECODE_DONE -> break
                        else -> throw SerializationException("unknown index: $index")
                    }
                }
            }
            return result ?: throw SerializationException("missing field: value|error")
        }
    }
    but to note,
    @JvmInline
    doesn't buy you anything if you're using
    Result<T>
    , so I don't see any reason to do that
    j

    Joost Klitsie

    11 months ago
    I thought it would be helpful to wrap it so if I have 1000 result objects or more that it would save me some resources, but I guess I have to read up a bit then on the specifics 🙂 thanks for the help @ephemient! I appreciate it
    e

    ephemient

    11 months ago
    same as primitives: using them in a generic way (via interface) always results in a box
    val number: Number = 1
    is a java.lang.Integer, not an int; ditto
    interface Result<T>
    if you really wanted to use
    @JvmInline
    , you have to pull shenanigans like
    kotlin.Result
    does:
    private class Failure(val error: CommonException)
    
    @JvmInline
    @Serializable(with = ResultSerializer::class)
    value class Result<out T> internal constructor(private val _value: Any?) {
        val isSuccess: Boolean
            get() = _value !is Failure
        val isFailure: Boolean
            get() = _value is Failure
        val value: T
            get() {
                check(_value !is Failure)
                return _value as T
            }
        val error: CommonException
            get() {
                check(_value is Failure)
                return _value.error
            }
    }
    
    fun <T> success(value: T): Result<T> = Result(value)
    
    fun failure(error: CommonException): Result<Nothing> = Result(Failure(error))
    
    class ResultSerializer<T>(private val tSerializer: KSerializer<T>) : KSerializer<Result<T>> {
        override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Result", tSerializer.descriptor) {
            element("value", tSerializer.descriptor)
            element("error", CommonException.serializer().descriptor)
        }
    
        override fun serialize(encoder: Encoder, value: Result<T>) {
            encoder.encodeStructure(descriptor) {
                if (value.isSuccess) {
                    encodeSerializableElement(descriptor, 0, tSerializer, value.value)
                } else {
                    encodeSerializableElement(descriptor, 1, CommonException.serializer(), value.error)
                }
            }
        }
    
        override fun deserialize(decoder: Decoder): Result<T> {
            var result: Result<T>? = null
            decoder.decodeStructure(descriptor) {
                while (true) {
                    when (val index = decodeElementIndex(descriptor)) {
                        0 -> result = success(decodeSerializableElement(descriptor, 0, tSerializer))
                        1 -> result = failure(decodeSerializableElement(descriptor, 1, CommonException.serializer()))
                        CompositeDecoder.DECODE_DONE -> break
                        else -> throw SerializationException("unknown index: $index")
                    }
                }
            }
            return result ?: throw SerializationException("missing field: value|error")
        }
    }
    christophsturm

    christophsturm

    11 months ago
    thats super interesting. you should blog about it to make it accessible outside of the slack
    j

    Joost Klitsie

    11 months ago
    @ephemient I managed to get the Failure and Loading to be serialized nicely (I had to use a serializer for the Nothing you proposed), but for the Success case I have one more hickup:
    @Serializable
    data class Test(val text: String)
    
    fun main() {
    	val successTest: Result<Test> = success(Test("Whatever"))
    	println("format: ${format.encodeToString(successTest)}")
    }
    Error:
    Class 'Test' is not registered for polymorphic serialization in the scope of 'Any'.
    Is there a way to mark the <T> to use whatever default serializer there is?
    for Failure I use this:
    @Serializable
    class Failure constructor(
    	val error: CommonException
    ) : Result<@Serializable(NotSerializable::class) Nothing>() {
    
    	@Contextual
    	override val value: Nothing?
    		get() = null
    
    }
    I was wondering if I can also add
    data class Success<T> (@Serializable(SOMETHING_HERE_PERHAPS?) override val value: T): Result<T>()
    I didn't make my own serializer for the result class, I use the abstract class as you suggested:
    @Serializable
    sealed class Result<out T> {
    	abstract val value: T?
    }
    
    @Serializable
    data class Success<T> constructor(override val value: T) : Result<T>()
    
    @Serializable
    data class Failure constructor(
    	val error: CommonException
    ) : Result<@Serializable(NotSerializable::class) Nothing>() {
    
    	@Contextual
    	override val value: Nothing?
    		get() = null
    
    }
    e

    ephemient

    11 months ago
    the auto-generated serializer seems to be confused with the super property. let me see if there's a more clever way
    j

    Joost Klitsie

    11 months ago
    this works fine:
    fun main() {
    	val failureTest: Result<Test> = failure(NotFoundException(""))
    	println("format: ${format.encodeToString(failureTest)}")
    }
    result:
    format: {"type":"com.klitsie.common.base.Failure","error":{"type":"NotFoundException","message":""}}
    the Success one indeed gets confused
    e

    ephemient

    11 months ago
    well, looking into it, it turns out PolymorphicSerializer and generics don't play well together. https://github.com/Kotlin/kotlinx.serialization/issues/1271
    the custom serializer should work though, and doesn't even need Success/Failure to be
    @Serializable
    complete example:
    j

    Joost Klitsie

    11 months ago
    @ephemient that works great! 🙂 If I change
    return result ?: loading()
    at the end, my Loading class is also supported 🙂 I guess that wouldn't be useful in practice, but good to know that I can 😄
    val successTest: Result<Test> = success(Test("Whatever"))
    	val failureTest: Result<Test> = failure(NotFoundException(""))
    	val loadingTest: Result<Test> = loading()
    	val successEncoded = format.encodeToString(successTest)
    	val failureEncoded = format.encodeToString(failureTest)
    	val loadingEncoded = format.encodeToString(loadingTest)
    
    	val successResult = format.decodeFromString<Result<Test>>(successEncoded)
    	val failureResult = format.decodeFromString<Result<Test>>(failureEncoded)
    	val loadingResult = format.decodeFromString<Result<Test>>(loadingEncoded)
    	println("success: $successResult")
    	println("failure: $failureResult")
    	println("loading: $loadingResult")
    	failureResult.onError { error ->
    results:
    success: Success(value=Test(text=Whatever))
    failure: Failure(error=com.klitsie.common.base.NotFoundException: )
    loading: Loading
    p

    pdvrieze

    11 months ago
    The problem you will be running into is that you will need to somehow get access to the serializer of either the success value or the serializer of the exception. For success you can use t type variable on Result, but for the exception you will need to have a serializer for the exception (reflection is possible, you could also use an inline refied parameter to get the serializer).
    j

    Joost Klitsie

    11 months ago
    @pdvrieze I made a serialization module where i add subclasses to the CommonException works like a charm 😃