Unable to use custom serializer for Javascript primitive types
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
bigintvalue
- 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
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, ... ]
For anyone that stumbles upon this, this is the dirty hack I did for serializer:
// create a serializer that will accept Any object, but ser/des it expecting BigInt
class JsLongSerializer() : KSerializer<Any> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("bigint.${Platform.Target}", PrimitiveKind.LONG)
override fun serialize(encoder: Encoder, value: Any) = encoder.encodeLong(value.unsafeCast<BigInt>().toString().toLong())
override fun deserialize(decoder: Decoder) = BigInt(decoder.decodeLong().toString())
}
// then configure json as so to get around compile-time generic type checks
val JSON = Json {
serializersModule = SerializersModule {contextual(PlatformLong::class, JsLongSerializer() as KSerializer<PlatformLong>)}
}
The end effect is instanceof type checks aren't added in generated JS code, only a nullability check added in generated code for the CCE case. So life's good with some dirty hacks.
I guess unsafeCast is the way to go. cc @JSMonk @broadwaylamb
@sandwwraith No, unsafeCast doesn't help here at all.
@shikharid, have you tried to use Long as BigInt compilation? Could it be a non-dirty hack solution specifically in this case (still the same problem applies to other JS primitives such as Symbol)
yup aware of that but it wasn't released when this was reported
it will take us a bit to get to 2.2.20 due to upstream projects, will adopt this then
@JSMonk enabling Long as BigInt won't fix the problem will it though, it seems to fix issues one way (export types with Long kotlin->js).
But if pure js code constructs an object with bigint's inside them and provides it to a kmp generated library which wants to serialise it using kotlinx-ser, things will still go through the same default serialisation path and raise CCE.. or i'm understanding it wrong?