Skip to content

Unable to use custom serializer for Javascript primitive types #3048

@shikharid

Description

@shikharid

Describe the bug
Have certain data classes which need to be de/serialised and contain JS types (primarily BigInt).
Added a custom serializer but serialisation always throws a ClassCastException.

Happy to contribute a fix, just give me some pointers.

To Reproduce

  • Try running following tests written for js bigint
@Serializable
data class ObjWithPrimitiveJsType(@Contextual val value: BigInt)

fun toBigInt(value: Long) = (js("BigInt") as (dynamic) -> BigInt)(value.toString())

class BigIntSerializer : KSerializer<BigInt> {
    override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("BigInt.js", PrimitiveKind.INT)
    override fun deserialize(decoder: Decoder): BigInt = toBigInt(decoder.decodeLong())

    override fun serialize(encoder: Encoder, value: BigInt) = encoder.encodeLong(value.toString().toLong())
}

class NativeLongTestsJs {

    fun `js primitive serializer bug`() {
        val data = ObjWithPrimitiveJsType(toBigInt(Random.nextLong()))
        val encoded = json.encodeToString(data)
        println(encoded)
    }
}

Expected behavior

  • Value should ser/deser properly, ser throws CCE instead
  • The culprit seems to be an overridden serialize method in the generated code which does a type check like:
  • Generated code:
  serialize_tl1wd4_k$(encoder, value) {
    return encoder.encodeLong_3didw_k$(toLong_0(toString_1(value)));
  }
  serialize_5ase3y_k$(encoder, value) {
    return this.serialize_tl1wd4_k$(encoder, value instanceof BigInt ? value : THROW_CCE());
  }
  • For JS primitive types, instanceof checks fail and one needs to do a "typeof" check instead iirc
  • This means kotlinx serialization can never be used to work with JS primitive types
    • Especially never for js types, as their BigInt doesn't even return a boxed value. It's just a constructor call that returns the primitive bigint value

Workarounds considered

  • Use a wrapper type for all primitive types, kinda dirty but solves the problem. Boilerplate for js devs who consume it.
  • Use "new Number" type constructors for creating primitive values, like:
    val jsNewNumber = js(
      "function(x) { return new Number(x); }"
    ) as (dynamic) -> JsNumber
    • This is not a solution as these objects can be passed directly from js apps in my case
    • And there I cannot control how these values are created (via constructor or notation)

Ideal Solution

  • Some way to mark a serializer js primitive so that it either avoids the instanceof checks or uses typeof check

Environment

  • Kotlin version: 2.1.20
  • Library version: 1.8.1
  • Kotlin platforms: JS for above issue (actual code is targets js/android/ios)
  • Gradle version: 8.7
  • IDE version (if bug is related to the IDE) [e.g. IntellijIDEA 2019.1, Android Studio 3.4]
  • Other relevant context [e.g. OS version, JRE version, ... ]

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions