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.eventAdam S
05/16/2024, 3:55 PMmessage.content.typeAdam 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 typeJoffrey
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"
}