Kotlin / kotlinx.serialization

Kotlin multiplatform / multi-format serialization
Apache License 2.0
5.4k stars 620 forks source link

"Serializer for class List is not found" on Native. #2770

Closed Nek-12 closed 1 month ago

Nek-12 commented 2 months ago
kotlinx.serialization.SerializationException: Serializer for class 'List' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.

Environment

We are using a reified function to deserialize response body from Ktor on Native using Darwin engine. The type parameter is an object containing a map with values of Lists of objects serialized using a polymorphic serializer. (I will add the example code a bit later).

On Android, the code works correctly without any modifications. Native however fails finding a serializer for List class, which is strange and looks like a compiler error. This is the only instance where we are using custom serializer modules / polymorphic so cannot really tell if this is something with the class.

Some sample code:

interface SyncedModel {

    val createdAt: Instant
    val updatedAt: Instant
    val id: Uuid
}

@Serializable
@SerialName("entry")
data class Entry(
    override val id: Uuid = uuid4(),
    val pointsDelta: Int,
) : SyncedModel 

@Serializable
enum class SyncedEntityType {
    Entry, //...
}

@Serializable
data class MultiSyncValue<out R : SyncedModel>(
    val items: List<R>,
    val deletedEntries: List<DeletedRecord>
)

@Serializable
internal data class MultiSyncRequest(
    val data: Map<SyncedEntityType, MultiSyncValue<SyncedModel>>,
    val lastSynced: Instant,
)

internal suspend inline fun <reified T, reified R> HttpClient.post(
    url: String,
    body: R? = null,
    builder: HttpRequestBuilder.() -> Unit = {},
) = call<T, R>(url, HttpMethod.Post, body, builder)

internal suspend inline fun <reified T, reified R> HttpClient.call(
    url: String,
    method: HttpMethod,
    body: R? = null,
    builder: HttpRequestBuilder.() -> Unit = {},
) = ApiResult {
    request(url.removePrefix("/")) {
        this.method = method
        body?.let {
            contentType(ContentType.Application.Json)
            setBody(it)
        }
        builder()
    }.body<T>()
}

internal val syncSerializerModule = SerializersModule {
    polymorphic(SyncedModel::class) {

        subclass(Entry.serializer())
    }
}

@OptIn(ExperimentalSerializationApi::class)
private fun Json() = Json {
    explicitNulls = false
    ignoreUnknownKeys = true
    coerceInputValues = true
    encodeDefaults = true
    prettyPrint = BuildFlags.debuggable
    useAlternativeNames = false
    decodeEnumsCaseInsensitive = true
    allowTrailingComma = true
    serializersModule = SerializersModule {
        include(syncSerializerModule)
        include(generalSerializersModule)
    }
}

client.post<List<SyncResult<SyncedModel>>, _>("/sync", MultiSyncRequest(data, lastSynced)) 
pdvrieze commented 2 months ago

What I suspect is that he "List" class is actually a native type, not kotlin.List one (for which ListSerializer is the actual serializer).

Nek-12 commented 2 months ago

Not sure about that since all that code is in common main source set and never left it. I am suspicious of the polymorphic serialization on native because that class / endpoint is the only one for which polymorphic serialization is used in the entire project.

Nek-12 commented 1 month ago

Guys, this is still affecting us. Still unable to run polymorphic requests on native. Kotlin 2.0.20 did not help. Do you have any workarounds in mind? I added some sample code

Nek-12 commented 1 month ago

Found a workaround:

client.post<String, _>("/sync", MultiSyncRequest(data, lastSynced)) {
        skipSavingBody()
    }.tryMap {
        json.decodeFromString(ListSerializer(SyncResult.serializer(PolymorphicSerializer(SyncedModel::class))), it)
    }

The key is to explicitly and manually deserialize from string without using reified. I am pretty sure it's an issue with how the typeOf() is handled by k/n

sandwwraith commented 1 month ago

I've tried to minimize reproducer a little big and got the following:

interface SyncedModel {
    val id: Int
}

@Serializable
@SerialName("entry")
data class Entry(
    override val id: Int = Random.nextInt(),
    val pointsDelta: Int,
) : SyncedModel

@Serializable
enum class SyncedEntityType {
    Entry, //...
}

@Serializable
data class MultiSyncValue<out R : SyncedModel>(
    val items: List<R>,
)

@Serializable
internal data class MultiSyncRequest(
    val data: Map<SyncedEntityType, MultiSyncValue<SyncedModel>>,
)

internal inline fun <reified T, reified R> post(
    url: String,
    body: R? = null,
    builder: () -> Unit = {},
): T = call<T, R>(url, "POST", body, builder)

internal inline fun <reified T, reified R> call(
    url: String,
    method: String,
    body: R? = null,
    builder: () -> Unit = {},
): T {
    println(jj.encodeToString(body))
    return jj.decodeFromString("""[{"type":"entry","id":0,"pointsDelta":42}]""")
}

internal val syncSerializerModule = SerializersModule {
    polymorphic(SyncedModel::class) {
        subclass(Entry.serializer())
    }
}

val jj = Json {
    serializersModule = syncSerializerModule
}

@Test
fun testReified() {
    println(post<List<SyncedModel>, _>("/sync", MultiSyncRequest(mapOf())))
}

On Native, it fails with

kotlinx.serialization.SerializationException: Serializer for class 'SyncedModel' is not found.
Please ensure that class is marked as '@Serializable' and that the serialization compiler plugin is applied.
To get enum serializer on Kotlin/Native, it should be annotated with @Serializable annotation.
To get interface serializer on Kotlin/Native, use PolymorphicSerializer() constructor function.

it is unfortunately true — because of https://youtrack.jetbrains.com/issue/KT-41339, we cannot get polymorphic serializer by KType, so specifying it explicitly as you've mentioned in workaround is the way to go.

Regardless List, however, I think it is a problem from Ktor side. From the looks of it, request function does not handle reified type arguments correctly. If you experience this problem with List of non-interface classes, I suggest reporting the issue directly to them.

Nek-12 commented 1 month ago

No, the only way this happens is with this polymorphic request. I think the cause is the issue you mentioned above, thanks, I didn't know about this caveat. I will keep track of that one