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

Serializer for Nothing not found

Open Octogonapus opened this issue 5 years ago • 19 comments
trafficstars

Describe the bug Using Nothing in a generic type argument fails with an internal error Serializer for element of type Nothing has not been found.

To Reproduce This reproduces the problem when compiling:

import kotlinx.serialization.Serializable

@Serializable
sealed class SerializableEither<out L, out R> {

    @Serializable
    data class Left<L>(val value: L) : SerializableEither<L, Nothing>()

    @Serializable
    data class Right<R>(val value: R) : SerializableEither<Nothing, R>()
}

Full compiler error: https://pastebin.com/FP6zGrDf

Expected behavior Nothing should be serializable.

Environment

  • Kotlin version: 1.3.60
  • Library version: 0.14.0
  • Kotlin platforms: JVM
  • Gradle version: 5.6.4
  • IDE version (if bug is related to the IDE): IntelliJ IDEA 2019.2.4 (Ultimate Edition)
  • Runtime version: 11.0.4+10-b304.77 amd64
  • Linux 5.0.0-36-generic

Octogonapus avatar Nov 24 '19 19:11 Octogonapus

I don't feel like Nothing should be serializable – since there is no legal way to have the instance of Nothing and property of type Nothing is meaningless.

However, what you want here is probably a @NotSerializable / @Serializable(NoSerializer) thing which may be useful also in cases like https://github.com/Kotlin/kotlinx.serialization/issues/607 (which slightly resembles this issue)

sandwwraith avatar Nov 25 '19 12:11 sandwwraith

I am of the perspective that Nothing is serializable because it contains no information: serizialing no information should be a trivial task, right? I originally found this problem because I was trying to serialize arrow-kt's data structures. arrow makes heavy use of Nothing throughout their data structures; most of them involve Nothing in one way or another.

@raulraja If you have a different perspective on this, please feel free to contribute it to this discussion.

Octogonapus avatar Nov 25 '19 13:11 Octogonapus

I think the issue here is not that Nothing is encodable or not. The issue here is that this is Nothing in type argument position. Nothing, bottom type, and subtype of all types can appear anywhere in generic arg in any data structure. For example:

sealed class Command<out A> {
  data class Message(val value: String): Command<String>()
  object Noop : Command<Nothing>()
}

Nothing in higher kind position still allows it's a wrapper to be instantiable if it Nothing appears as the type arg of a covariant argument like out A

Nothing itself does not need to be instantiated or serialized because the bottom type has 0 inhabitants.

Any? = All values
Unit = One singleton value
Nothing = No value

I think all Serialization needs for this to work with is to ignore Nothing when it appears in type argument position.

Revisiting a simple understood example which is object to this problem. One of the Arrow Data types.

A simplified version:

sealed class Option<out A> {
  data class Some<out A>(val value: A): Option<A>()
  object None : Option<Nothing>()
}

val someOne: Option<Int> = Some(1)
val noOne: Option<Int> = None

In this example where Option = Some | None that is a sealed union in order to provide a Serializer, it needs the Serializers of Some, None and A. But it does not need the serializer of Nothing to provide any of the possible values that can be created for an Option.

val kindedUpperBound: Option<Any?> = Some(1)
val kindedLowerBound: Option<Int> = Some(1)
val kindedNothing: Option<Nothing> = Some(1) //does not compile because Option<Int> is not a subtype of Option<Nothing>

In the case above since Nothing appears invariant in the receiver value it can ensure impossible values won't compile.

I believe you can support higher kinded <_ : Nothing> which is the issue at hand by simply treating Nothing in type argument position as a NOOP serializer and keep bailing for properties which specify Nothing directly. For example:

class ImpossibleToSerializeOrInstantiate(val x: Nothing = TODO())

raulraja avatar Nov 26 '19 16:11 raulraja

Perhaps a NothingSerializer would be valid if it would indeed refuse to serialize or deserialize (but does exist for type argument reasons). I think the use case is valid and implementing is probably easiest when just creating this "serializer"..

pdvrieze avatar Nov 26 '19 22:11 pdvrieze

So it would be basically like this:

object NotSerializable : KSerializer<Any> {
    override val descriptor: SerialDescriptor = SerialClassDescImpl( "kotlin.Any") 
    override fun deserialize(decoder: Decoder): Any = throw SerializationException("Not serializable")
    override fun serialize(encoder: Encoder, obj: Any) = throw SerializationException("Not serializable")
}

Such serializer can be applied for types in generic arguments (data class Left<L>(val value: L) : SerializableEither<L, @Serializable(NotSerializable::class) Nothing>()).

It even can be a default serializer for Nothing.

sandwwraith avatar Dec 03 '19 13:12 sandwwraith

@sandwwraith The way I see it yes (except perhaps the return type for deserialize could be Nothing).

pdvrieze avatar Dec 03 '19 16:12 pdvrieze

I've just started using kotlinx-serialization in a new project and this is basically the first issue I came across, with a compiler error that's not very helpful (location: "File is unknown").

Having a default serializer for Nothing would avoid that, wouldn't it?

fluidsonic avatar Jul 18 '21 18:07 fluidsonic

I am trying to use kotlinx-serialization and I ran into this error.

sealed class ApiResponse<out S, out E> {
    data class SuccessResponse<S>(val response: S): ApiResponse<S, Nothing>()

    object SuccessNoBody: ApiResponse<Nothing, Nothing>()

    data class ErrorResponse<E>(val error: E): ApiResponse<Nothing, E>()
}

Any help would be great! Thanks in advance :)

rubenquadros avatar Nov 23 '21 17:11 rubenquadros

@rubenquadros Assuming that the error you are getting refers to there not being a serializer for Nothing you would need to create a custom serializer for Nothing. Something like:

object NothingSerializer: KSerializer<Nothing> {
    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("kotlin.Nothing") {}

    override fun deserialize(decoder: Decoder): Nothing =
        throw UnsupportedOperationException("Nothing does not have instances")

    override fun serialize(encoder: Encoder, value: Nothing): Nothing =
        throw UnsupportedOperationException("Nothing cannot be serialized")
}

pdvrieze avatar Nov 24 '21 19:11 pdvrieze

@pdvrieze I still get the same error. I am using kotlinx-serialization with ktor. Maybe I am doing something wrong. Here is my code

    runBlocking {
        client.responsePipeline.intercept(HttpResponsePipeline.Transform) { (info, body) ->
            val response = when {
                context.response.status == HttpStatusCode.OK -> {
                    if (context.response.contentLength() == 0L) ApiResponse.SuccessNoBody
                    else ApiResponse.SuccessResponse(body)
                }
                else -> {
                    ApiResponse.ErrorNoBody(context.response.status.value)
                }
            }
            proceedWith(HttpResponseContainer(info, response))
        }
        val response: ApiResponse<User, Nothing> = client.post("url")
    }

rubenquadros avatar Nov 25 '21 16:11 rubenquadros

@rubenquadros You will need to actually make sure that you actually specify the serializer within ApiResponse. Something like: Every place it has Nothing as parameter you use @Serializable(NothingSerializer::class) Nothing to expose nothing. We really need the error message though.

pdvrieze avatar Nov 25 '21 19:11 pdvrieze

@pdvrieze Here is the stacktrace https://pastebin.com/pNYVKCyn Sorry for not providing it earlier.

rubenquadros avatar Nov 25 '21 19:11 rubenquadros

@rubenquadros The problem here is that you somehow captured a Void type from the Java side. This is not a built in type. Ideally you would avoid the Void type entirely, but alternatively you could provide a serializer as a contextual serializer (registered in the module).

pdvrieze avatar Nov 25 '21 20:11 pdvrieze

Good point. I tried this first but it doesn't work for the JS target. Now the only workaround I can think of is creating a SerializableNothing class and replacing Nothing with it everywhere. I am wondering if there is a multiplatform workaround other than this and simpler than this.

ShreckYe avatar Feb 13 '22 14:02 ShreckYe

Hi everyone. I have tried both workaround solutions, using contextual serializers and mapping Nothing to SerializableNothing, and it turned out the latter works better on Multiplatform. I managed to organize some prototype code in an open-source library, the core of which is this KType.mapNothingToSerializableNothing function. I hope it can provide a good reference.

If you'd like to you can also depend the library from Maven Central. The code is still in prototype and I didn't add any tests yet so there is not much guarantee. However, we are already using it in our internal projects.

ShreckYe avatar Jun 03 '22 08:06 ShreckYe

So it would be basically like this:

object NotSerializable : KSerializer<Any> {
    override val descriptor: SerialDescriptor = SerialClassDescImpl( "kotlin.Any") 
    override fun deserialize(decoder: Decoder): Any = throw SerializationException("Not serializable")
    override fun serialize(encoder: Encoder, obj: Any) = throw SerializationException("Not serializable")
}

Such serializer can be applied for types in generic arguments (data class Left<L>(val value: L) : SerializableEither<L, @Serializable(NotSerializable::class) Nothing>()).

It even can be a default serializer for Nothing.

@sandwwraith Any plans on moving this forward?

joffrey-bion avatar Jun 20 '22 15:06 joffrey-bion

Not having a Nothing implementation by default makes using kotlinx.serialisation impossible with any libraries using ~~higher kinded types~~ parameterised types. Since the types are not owned by application authors in such cases the workaround of annotating the definition site can not be used as well.

phanimahesh avatar Jul 13 '22 04:07 phanimahesh

I guess you mean generic/parameterized types instead of higher-kinded types. If I am not wrong, higher-kinded types are not supported in Kotlin.

ShreckYe avatar Jul 13 '22 16:07 ShreckYe

My bad, yes. I meant parameterised types that use Nothing, which isn't uncommon.

phanimahesh avatar Jul 13 '22 17:07 phanimahesh