Skip to content

SerializersModule.serializer() returns normally for interfaces that aren’t serializable #1764

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
swankjesse opened this issue Nov 18, 2021 · 2 comments
Assignees
Labels

Comments

@swankjesse
Copy link

Describe the bug
I’m using SerializersModule.serializer() to look up a serializer for my class when my service starts up. If a serializer is not available I expect this function to throw an exception. That way if there’s a configuration problem I find out early. Otherwise I only crash when I attempt to use the serializer.

Unfortunately kotlinx.serialization returns successfully when looking up the serializer for an interface type, even if no polymorphic serializers are registered for that type.

This is inconsistent with the behavior of serializers for concrete classes. In this case kotlinx.serialization throws an exception when requesting a serializer for a class that is not serializable.

To Reproduce
This case looks up serializers for 4 cases: serializable class, serializable interface, non-serializable class, and non-serializable interface. We expect the serializable types to return normally, and the non-serializable types to throw an exception. Unfortunately the test method nonSerializableInterface() doesn't throw an exception.

import kotlin.test.assertFailsWith
import kotlinx.serialization.Serializable
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.serializer
import org.junit.Test

class LookupInterfaceSerializerTest {
  @Test
  fun serializableClass() {
    // This class is serializable so this completes normally.
    serializersModule.serializer<SerializableClass>()
  }

  @Test
  fun serializableInterface() {
    // This interface is serializable so this completes normally.
    serializersModule.serializer<SerializableInterface>()
  }

  @Test
  fun nonSerializableClass() {
    // This class is not serializable so requesting a serializer fails.
    assertFailsWith<IllegalArgumentException> {
      serializersModule.serializer<NonSerializableClass>()
    }
  }

  @Test
  fun nonSerializableInterface() {
    // This interface is not serializable so requesting a serializer should fail. Unfortunately
    // kotlinx.serialization succeeds here, but will crash later when the serializer is used.
    assertFailsWith<IllegalArgumentException> {
      serializersModule.serializer<NonSerializableInterface>()
    }
  }
}

private val serializersModule = SerializersModule {
  polymorphic(
    SerializableInterface::class,
    RealSerializableInterface::class,
    RealSerializableInterface.serializer()
  )
}

@Serializable
class SerializableClass(val message: String)

interface SerializableInterface {
  val message: String
}

@Serializable
class RealSerializableInterface(
  override val message: String
) : SerializableInterface

class NonSerializableClass(val message: String)

interface NonSerializableInterface {
  val message: String
}

Expected behavior
Exception thrown.

Environment

  • Kotlin version: 1.5.31
  • Library version: 1.3.1
  • Kotlin platforms: JVM
  • Gradle version: 7.0.2
@sandwwraith
Copy link
Member

sandwwraith commented Nov 18, 2021

That's an interesting design issue. Normally, I would say that val serial = serializer<SomeInterface> should always return a polymorphic serializer because we can use it with any format and any module. However, top-level serializer<T>() is defined as EmptyModule.serializer<T>() and here you raise the issue about extension function and that's a tricky part, because actual lookup is performed in the format, not on the receiver module. As a consequence the following code works too:

val moduleThatContainsSubclasses = SerializersModule { ... }

val json = Json { module = moduleThatContainsSubclasses}
json.encodeToString(moduleThatDoesNotContainAnythingRelated.serializer<SomeInterface>, ...)

because actual lookup of subclasses is performed inside the module that passed to Json instance.

@sandwwraith
Copy link
Member

I do not think we are able to do something with it at this point — prohibiting resolving serializer<SomeInterface>() to polymorphic serializer is not correct, because such serializer can be used in any format which provides sufficient SerializersModule. SomeInterface is also treated as implicitly @Serializable by compiler, so there should be a feature parity. We did some tweaks (namely, context serializers now have priority over polymorphic for interfaces: #2060) to ease this problem, but it for now it is left as-is.

@sandwwraith sandwwraith closed this as not planned Won't fix, can't repro, duplicate, stale Dec 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants