Ray Rahke
05/16/2024, 2:50 PM// A:1
{
"type": "game",
"event": "NPC spawned"
"who": "id2134789s3"
"where": [500, 500]
}
// A:2
{
"type": "game",
"event": "NPC death",
"who": "id2134789s3",
"where": [500, 500]
}
// A:3
{
"type": "game",
"event": "quest completed"
"title": "A New Day..."
}
// B:1
{
"type": "connection",
"event": "player connected"
"username": "user538"
}
// B:2
{
"type": "connection",
"event": "player disconnected",
"username": "user538"
}
// B:3
{
"type": "connection",
"event": "message sent"
"msgcontent": "hello!",
"sender": "user538",
"receiver": "user998"
}
I assume I am supposed to do this using sealed interfaces and nested data classes... or something else? (I do not want to hard code it. I want type safety and intellisense)
You can see the are two main categories "game"
"connection"
, that each have their own sub event categories, and each sub event category is associated with it's own data variables like "who"
or "sender"
. So a single flat sealed interface/enum is not going to be adequate.
This is pretty trivial in typescript because of their literal string subsets and discriminated unions / mapped interfaces techniques. But seeming tricky in KotlinAdam S
05/16/2024, 3:08 PMAdam S
05/16/2024, 3:09 PMRay Rahke
05/16/2024, 3:13 PMAdam S
05/16/2024, 3:14 PMRay Rahke
05/16/2024, 3:15 PMAdam S
05/16/2024, 3:18 PMAdam S
05/16/2024, 3:20 PMtype
and event
fields. How important is that that they're seperate? It'd be a lot more convenient for KxS to have them as a single field.
Are you only expecting to use the JSON from Kotlin?Ray Rahke
05/16/2024, 3:22 PMif (data.type == "game")
data.event // TS infers this cannot possibly be "message sent", only things like "NPC spawn", etc.
Ray Rahke
05/16/2024, 3:23 PMRay Rahke
05/16/2024, 3:23 PMRay Rahke
05/16/2024, 3:23 PMRay Rahke
05/16/2024, 3:24 PMRay Rahke
05/16/2024, 3:25 PMAdam S
05/16/2024, 3:27 PMRay Rahke
05/16/2024, 3:28 PMRay Rahke
05/16/2024, 3:28 PMsealed interface Message {
val type: String
sealed interface GameMessage : Message {
override val type: String = "game"
data class NpcSpawned(val who: String, val where: List<Double>) : GameMessage
data class NpcDeath(val who: String, val where: List<Double>) : GameMessage
data class QuestCompleted(val title: String) : GameMessage
}
sealed interface ConnectionMessage : Message {
override val type: String = "connection"
data class PlayerConnected(val username: String) : ConnectionMessage
data class PlayerDisconnected(val username: String) : ConnectionMessage
data class MessageSent(val msgcontent: String, val sender: String, val receiver: String) : ConnectionMessage
}
}
Ray Rahke
05/16/2024, 3:28 PMAdam S
05/16/2024, 3:28 PMAdam S
05/16/2024, 3:31 PMRay Rahke
05/16/2024, 3:39 PMAdam S
05/16/2024, 3:39 PMAdam S
05/16/2024, 3:39 PMRay Rahke
05/16/2024, 3:40 PMRay Rahke
05/16/2024, 3:40 PMRay Rahke
05/16/2024, 3:40 PMRay Rahke
05/16/2024, 3:40 PMRay Rahke
05/16/2024, 3:41 PMRay Rahke
05/16/2024, 3:41 PMAdam S
05/16/2024, 3:41 PMAdam S
05/16/2024, 3:45 PM"type"
field. There's no 'shape based' decoding (out of the box, anyway - you can roll your own). And having multiple 'type' fields is possible, but it's kind of a pain to do it manually.
What will probably be better is to split up the main event types from the subtypes, so you'll have a nested object:
{
"type": "game",
"content": {
"type": "NPC spawned"
"who": "id2134789s3"
"where": [500, 500]
}
}
Adam S
05/16/2024, 3:45 PMAdam S
05/16/2024, 3:46 PMtype
field - that'll be added automatically by KxSAdam S
05/16/2024, 3:47 PMif (message.type == "game")
, and in Kotlin you'd do if (message is Message.Game)
Ray Rahke
05/16/2024, 3:54 PMmessage.event
Adam S
05/16/2024, 3:55 PMmessage.content.type
Adam S
05/16/2024, 3:57 PMtype
is the default JSON discriminator in KxS - you can rename it back to event
if you like - see @JsonClassDiscriminator
https://github.com/Kotlin/kotlinx.serialization/blob/v1.6.3/docs/json.md#class-discriminator-for-polymorphismRay Rahke
05/16/2024, 4:17 PMRay Rahke
05/16/2024, 4:17 PMRay Rahke
05/16/2024, 4:17 PMRay Rahke
05/16/2024, 4:18 PMRay Rahke
05/16/2024, 4:20 PMRay Rahke
05/16/2024, 4:21 PMRay Rahke
05/16/2024, 4:24 PMval jsonData = Json.encodeToString(MyModel.serializer(), MyModel(42))
where MyModel
is the specific data class...Ray Rahke
05/16/2024, 4:24 PMRay Rahke
05/16/2024, 4:41 PMAdam S
05/16/2024, 5:09 PM@Serializable
so that the plugin generates a serializer is the easiest way. But @Serializable
has some requirements, like the constructor args must be properties.Adam S
05/16/2024, 5:16 PMsealed interface GamePackets {
@Serializable
@SerialName("connection/message sent")
data class ConnectionMessageSent(
val msgContent: String
): GamePackets
// more packets ...
}
That'd create JSON like this
{
"type": "connection/message sent",
"msgContent": "x"
}
Adam S
05/16/2024, 5:17 PMJson.encodeToString(MyModel(42))
, and KxS will figure out the correct serializer. There's an example here.Ray Rahke
05/16/2024, 6:11 PMclass Bar { }
class Foo {
bar: Bar
}
@Serializable
data class Context(val a: Int, val obj: Foo) {}
Do all three of these classes need to be marked as @Serializable?Ray Rahke
05/16/2024, 6:12 PMAdam S
05/17/2024, 7:25 AMJoffrey
05/17/2024, 10:52 AMIf a language lacks a powerful type system and makes very simple mundane stuff like arbitrarily nested json objects tedious, I am just going to bypass itThe language allows nested sealed hierarchies exactly like you need. Serializing those as JSON, there is no need to express every hierarchy level in the resulting JSON, that's just a waste. You only ever need one "type" field no matter how deep the hierarchy is. For instance, if your
type
is NpcDeath
, you don't need any extra information to know that it's a GameMessage
and a Message
.Joffrey
05/17/2024, 10:54 AMtype
and event
is a bit redundant, because if the recipient of the message knows the type hierarcy, event
should be sufficient to deduce type
Joffrey
05/17/2024, 10:57 AMsealed interface Message {
sealed interface GameMessage : Message {
data class NpcSpawned(val who: String, val where: List<Double>) : GameMessage
data class NpcDeath(val who: String, val where: List<Double>) : GameMessage
data class QuestCompleted(val title: String) : GameMessage
}
sealed interface ConnectionMessage : Message {
data class PlayerConnected(val username: String) : ConnectionMessage
data class PlayerDisconnected(val username: String) : ConnectionMessage
data class MessageSent(val msgcontent: String, val sender: String, val receiver: String) : ConnectionMessage
}
}
The type
field will be handled by Kotlinx Serialization automatically at every level in your hierarchy where a polymorphic type is usedJoffrey
05/17/2024, 10:59 AM// A:1
{
"type": "your.package.NpcSpawned",
"who": "id2134789s3",
"where": [500, 500]
}
// A:2
{
"type": "your.package.NpcDeath",
"who": "id2134789s3",
"where": [500, 500]
}
// A:3
{
"type": "your.package.QuestCompleted",
"title": "A New Day..."
}
// B:1
{
"type": "your.package.PlayerConnected",
"username": "user538"
}
// B:2
{
"type": "your.package.PlayerDisconnected",
"username": "user538"
}
// B:3
{
"type": "your.package.MessageSent",
"msgcontent": "hello!",
"sender": "user538",
"receiver": "user998"
}