Hi all! I have sort of my own take on the Kotlin R...
# serialization
j
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 ๐Ÿ™‚
Copy code
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
using
@JvmInline
like that is rather pointless, since it's only accessible via the public interface which will box itโ€ฆ
๐Ÿ‘ 1
j
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
if you just used a sealed class and made some minor adjustments, then everything would work right out of the box:
Copy code
@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:
Copy code
@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
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
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:
Copy code
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")
    }
}
๐Ÿ’ฏ 1
c
thats super interesting. you should blog about it to make it accessible outside of the slack
j
@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:
Copy code
@Serializable
data class Test(val text: String)

fun main() {
	val successTest: Result<Test> = success(Test("Whatever"))
	println("format: ${format.encodeToString(successTest)}")
}
Error:
Copy code
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:
Copy code
@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:
Copy code
@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
the auto-generated serializer seems to be confused with the super property. let me see if there's a more clever way
j
this works fine:
Copy code
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
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
@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 ๐Ÿ˜„
Copy code
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:
Copy code
success: Success(value=Test(text=Whatever))
failure: Failure(error=com.klitsie.common.base.NotFoundException: )
loading: Loading
p
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
@pdvrieze I made a serialization module where i add subclasses to the CommonException works like a charm :)