What are your strategies for sharing DTOs with JS/...
# javascript
c
What are your strategies for sharing DTOs with JS/TS code?
Copy code
@JsExport
@Serializable
data class Foo(
    val bar: List<String>
)
is not exportable because
List
is not exportable.
Copy code
@JsExport
@Serializable
data class Foo(
    val bar: Array<String>
)
doesn't work, because array equality is identity-based and not content-based.
Copy code
@JsExport
@Serializable
class Foo(
    val bar: Array<String>
) {
    override fun equals(…) = …
    override fun hashCode() = …
}
works, but it's a shame to have to generate
equals
and
hashCode
on all objects when one of the big advantages of Kotlin is not having to do this, especially for DTOs.
Copy code
@JsExport 
@Serializable
class MyCustomArray<T>(
    val data: Array<T>
) { … }

@JsExport
@Serializable
class Foo(
    val bar: MyCustomArray<T>,
)
doesn't work because KotlinX.Serializable can't serialize generic arrays.
Copy code
@JsExport
@Serializable
class MyCustomList<T> private constructor(
    internal val data: List<T>
) {
    val elements: Array<T>
        get() = ⚠
}

@JsExport
@Serializable
class Foo(
    val bar: MyCustomList<T>
)
doesn't work because we can't convert a generic list into a generic array. I'm curious, how do people share DTOs with JS? I see that the other solution is creating a dedicated JS shim, but the entire point of sharing code is to avoid duplication.
a
List
is exportable since 2.0
👀 1
So, the first code snippet should be fine
u
custom classes working fine. but i've blob data want to send it to server.
e
how do people share DTOs with JS
I have a HUGE amount of mapping code in the TS side. That's it really.
My colleagues hate me for that lmao
I've also been discussing about it recently in another issue with Artem and Victor, but if you're looking for a way to have plain object-like Kotlin classes, that you can serialize/deserialize, the most immediate way would be to have a
@JsField
annotation, to avoid wrapping properties into accessors.
t
Kotlin data classes: 1. Bad as source for
JSON.stringify
2. Bad as payload for messaging (Window <-> Window, Window <-> Worker) 3. Bad for messaging with other JS application
For messaging we created special marker type Serializable (56 types already marked)
For communication with JS app best option, which I see - serialization to JSO (already exists in
kotlinx-serialization
)
Single problem - missed JSO signature, which can be generated by compiler plugin
c
List
is exportable since 2.0
Oh, that could solve all our problems then… I'll take a look
@turansky The use-case here is to share DTOs between client (JS/JVM) and server (JVM)
Is there a plan for exportable enums?
The documentation of
JsExport
should probably be updated to mention
List
/`Set` /`Map`, it currently only mentions
Array
:https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.js/-js-export/
t
Exportable
List
,
Set
,
Map
- fine if Kotlin/JS is producer and doesn't receive data back. In my previous cases communication was bidirectional and both parts (JS and Kotlin/JS) modified data. In common case it means that data, which you provide should be convertable to JSON or cloneable (depends on your case).
a
> Is there a plan for exportable enums? Are they not exportable? I've just checked in the playground and it doesn't say they are not
> Exportable
List
,
Set
,
Map
- fine if Kotlin/JS is producer and doesn't receive data back. Why? Since 2.0 you can also create Kotlin List/Set/Map from JavaScript Array/Set/Map
The documentation of
JsExport
should probably be updated...
Yes, it's true. However we want firstly to finish all the planned work on the @JsExport and fully update the documentation about JsExport
e
I always refer to https://kotlinlang.org/docs/js-to-kotlin-interop.html#kotlin-types-in-javascript for exportable types, which is up to date.
c
Are they not exportable? I've just checked in the playground and it doesn't say they are not
The documentation of
@JsExport
says they are not
@Edoardo Luppi that page doesn't mention enums
e
I'll open a PR to add it then
c
TypeScript isn't happy about the generated .d.ts
e
Strange. Might be how your TS project is configured? I've been exporting and using enums from TS for a long time.
🤔 1
No warnings or errors in IJ.
Noticed that kotlinx-serialization is creating a companion that shouldn't be exported. Worth letting them know probably.
h
@turansky Sorry to hijack the thread, but about data class being bad. We currently use them extensively in our KotlinJS App the data classes are serialized and deserialized with kotlinx on the client and server side. Is this a problem or was them being bad/slow about TS/JS interop?
t
Data classes are cool if you use them inside Kotlin application only. Exporting of data classes is bad idea from my perspective. In common case JSO/JSON - preferable options for communication with JS world.
h
Ah okay, no we don't export them, it's just inside KotlinJS (react). Thank you. edit: And for wrapping/communication we always use
external interface
and
jso
t
You can use
@JsPlainObject
, created by @Artem Kobzar to avoid unsafe
jso
calls 😉
@Edoardo Luppi I have pill for you - it's OpenAPI 😉
👀 1
From OpenAPI you can generate all required contracts 😉 Both data classes and JSO
h
Thanks, I will look it up 👍
t
... And
toJSON
will be generated for JS platform only (if you need it)
c
I don't understand. The main selling point of using KMP to me is the ability to share domain objects/DTOs/etc between client and server. But you're saying we shouldn't do that, and have dedicated classes on the JS side anyway? If they're going to be generated by OpenAPI anyway, that's not reducing the amount of code from just using TS.
e
and have dedicated classes on the JS side anyway
No that shoud not be the case, not always at least. At the moment, we can definitely export Kotlin DTOs to JS, be them data classes or not, and have a JS consumer use them, and even create new instances. The problem arises when the exported DTOs need to be serialized and deserialized (e.g. to go through a messaging system, like
postMessage
BroadcastChannel
). In this specific case, you need "mapping" plain objects on the JS side, as the exported Kotlin DTOs cannot correctly serialize.
t
Kotlin DTOs cannot correctly serialize
And regular JS classes have definitely the same problem
e
And regular JS classes have definitely the same problem
If they use accessors, yes. If they use regular fields, no.
a
In case it helps, I made a library for generating TypeScript from KxS types. I made it specifically for helping integrate a pure TypeScript application with a Kotlin JVM backend (via json encoded messages). I made it before Kotlin had TypeScript exporting, so maybe that's more practical. https://github.com/adamko-dev/kotlinx-serialization-typescript-generator
t
Thank you @Adam ! It's cool demonstration of runtimeless TS contracts, which I tried to describe in this thread. cc @Sergei Grishchenko
c
The problem arises when the exported DTOs need to be serialized and deserialized (e.g. to go through a messaging system, like
postMessage
BroadcastChannel
). In this specific case, you need "mapping" plain objects on the JS side, as the exported Kotlin DTOs cannot correctly serialize.
Ah, you mean serialize using the native JS serialization? Because in my case, serialization is done through KotlinX.Serialization.
Also, KotlinX.Serialization has
encodeToDynamic
and
decodeFromDynamic
. Wouldn't that solve the native JS serialization issues?
t
In most cases you will receive fine JSO, which can be used for messaging.
Messaging rules are strict, but we can check them on compilation time
c
Isn't that already what
encodeToDynamic
does?
t
Copy code
// Kotlin common
// For commutnication in Kotlin
@Serializable
data class UserDTO(
    val name: String,
    val addresses: List<String>,
)

// Kotlin/JS/wasmJS
// For messaging
@JsPlainObject 
external interface User {
    val name: String
    val addresses: ReadonlyArray<String>,
}

// TS
type User = Readonly<{
    name: string
    addresses: readonly string[]
}>

// usage
fun main() {
    val user = UserDTO(...)
    sendToJS(encodeToDynamic(user) /* User */)
}
Isn't that already what
encodeToDynamic
does?
Result can be non-serializable in JS terms
c
In which cases?
t
If custom serializer will provide incompatible type (anybody outside this list)
c
When using the default serializers generated by KotlinX.Serialization, nothing outside of this list is used, correct?
t
I hope
😅 1
By default I expect only objects, arrays and primitives
e
Also, KotlinX.Serialization has
encodeToDynamic
and
decodeFromDynamic
. Wouldn't that solve the native JS serialization issues?
Yes, but using them outside of the Kotlin realm is mission impossible at the moment. Assume you're in TS, and you have an instance of a Kotlin DTO, which you now have to serialize through
postMessage
, how are you going to do it? It wouldn't be a problem if the DTO properties were exposed as fields, but the are always wrapped currently.
c
I see.
Still, creating a
MyDto.serialize(): dynamic = encodeToDynamic(this)
and back seems like a lot less work than creating JsPlainObject duplicates of all classes + mapping functions, no?
e
Imagine you have hundreds of DTOs, are you going to export a decode and encode function for each type?
c
If adding an encode/decode function to each DTO is too much work, I don't see how having a
@JsPlainObject
duplicate + Kotlin functions to convert back/from the DTO objects is any less work
e
Oh I get it, and I agree. I was trying to do something better when I encountered a compiler bug, see https://youtrack.jetbrains.com/issue/KT-77372/KJS-NullPointerException-at-JsIntrinsicsJsReflectionSymbols#focus=Comments-27-11983871.0-0
The problem is, to have a generic (de)serialization mechanism, you need access to the
KClass
t
In my schema all
encode/decode
operations you do only inside Kotlin/JS application
In exported methods you use JSO interfaces
e
But doesn't that mean having a generated
interface
per
class
to represent the plain object? Plus it becomes a pain as now you're specializing the JS side, while the KMP mantra should be keeping as much as possible in
commomMain
t
Copy code
// Common
@Serializable
data class UserDTO

// JS only
@JsPlainObject
external interface UserJSO
e
But then you also need specialized functions under
jsMain
to complement the interfaces. In my case I export classes and functions from
commonMain
, and trying to do stuff under
jsMain
would increase complexity quite a lot.
How are you going to solve this?
Copy code
// commonMain
@JsExport
class DtoProducer {
  fun produce(): Dto = ...
}

@JsExport
class Dto(...)
Even if I have an interface such as
Copy code
// jsMain
@JsPlainObject
external interface DtoPlain
What do I do with
DtoProducer
?
I can workaround as much as you want by creating facades under
jsMain
, but that's me. Proposing such a thing in a team (10-15 people) would be impossible as now everyone has to be familiar with the workaround mechanism.
t
My previous answers were about simple use case 😉
✔️ 1
c
Hey, that's me! xD
To share a bit more about the use-case, we have a large codebase with a Kotlin backend and an Angular frontend. We're trying to make it easier to evolve, and one of the obvious steps is the removal of the DTO duplication and having constants in shared code describing API endpoints.
a
@CLOVIS, just to summarize the thread for myself, is it true that the first described case (in the code snippet below) works for you with the Kotlin collections exportability, or is there something else we can do for the described use case? (you mentioned invalid TS declarations, I want to understand if the issue is existing)
Copy code
@JsExport
@Serializable
data class Foo(
    val bar: List<String>
)