ursus
08/04/2026, 2:41 PM[
{
"type" : "ui", <-------
"ui-type" : "card", <-------
"heading" : "Heading"
},
{
"type" : "command", <-------
"command-type" : "emailApproved", <-------
"email" : "email@email.com"
},
{
"type" : "command", <-------
"command-type" : "openOnboarding" <-------
}
]
which has the usual type discriminator
can I have it have a secondary discriminator, while keeping it all in annotations only?
@Serializable
sealed class Item {
@Serializable
@SerialName("ui")
@JsonClassDiscriminator("ui-type")
sealed class Ui : Item() {
data class Card(val heaading: String)
}
@Serializable
@SerialName("command")
@JsonClassDiscriminator("command-type")
sealed class Command : Item() {
@Serializable
@SerialName("emailApproved")
data class EmailApproved(val email: String) : Command()
@Serializable
@SerialName("openOnboarding")
data object OpenOnboarding : Command()
}
}
I naively modeled it as this, but doesnt work
I know I for sure can make anything/it work with custom deserializer
but I prefer not to
So is there a annotation-only way I'm not seeing?Vsevolod Artiushkin
08/12/2026, 2:48 PMJuan Hurtado
08/24/2026, 6:16 AM@GhostSerialization(discriminator = "type")
sealed class Item {
@GhostSerialization(name = "ui", discriminator = "ui-type")
sealed class Ui : Item() {
@GhostSerialization(name = "card")
data class Card(val heading: String) : Ui()
}
@GhostSerialization(name = "command", discriminator = "command-type")
sealed class Command : Item() {
@GhostSerialization(name = "emailApproved")
data class EmailApproved(val email: String) : Command()
@GhostSerialization(name = "openOnboarding")
data object OpenOnboarding : Command()
}
}
// Ktor/Ghost first, KotlinX fallback for everything else
install(ContentNegotiation) {
ghost()
json(Json { ignoreUnknownKeys = true })
}
type → `Ui`/`Command`, then ui-type / command-type → leaf. No custom deserializer for reading.
github.com/juanchurtado1991/ghost-serializer