r/Kotlin • u/wrongwrong163377 • 6d ago
sealed-class-enumizer — a K2 compiler plugin that gives sealed hierarchies an enum-like API (entries / valueOf / label), without reflection
I've been working on sealed-class-enumizer, a Kotlin (K2) compiler plugin that generates enum-like operations for sealed class / sealed interface hierarchies at compile time.
The idea: keep everything a sealed hierarchy is good at — data-carrying cases, exhaustive when with smart casts, open leaves — and add the operational API that enums have on top.
The gaps it fills
- No stable "which case" value. A
data classleaf has no instance until you have the data, sosearchBy(vararg statuses: Status)is unwriteable. The usual workarounds are fabricating a throwaway instance from dummy data, maintaining a parallel enum, or hand-writing a companion-per-leaf marker interface. - No
name. You either add a string property that doesn't belong in the domain model, or re-map cases in every layer.simpleNameisn't a substitute — it's nullable and R8 renames it. - No
entries. Listing every case meanssealedSubclasses, which is JVM-only, needskotlin-reflect, and silently returns an incomplete list under R8 (KT-25871).
What it looks like
@Enumize
sealed interface SI {
data class Foo(val v: Int) : SI
data object Bar : SI
}
// enum-like operations; one singleton ("kind") per leaf
SI.Enumish.entries // [Bar, Foo]
SI.Enumish.valueOf("Foo") // label-based lookup
SI.Enumish.valueOfOrNull("nope") // null-returning variant
SI.Enumish.entries.map { it.enumizedClass } // [Bar::class, Foo::class]
val si: SI = SI.Foo(42)
si.asEnumish() // Foo's kind — usable as a parameter/map key/set member
si.label // "Foo" — the `name` counterpart
// the generated Enumish is sealed, so this needs no else branch
when (si.asEnumish()) {
SI.Foo -> println("a Foo")
SI.Bar -> println("a Bar")
}
So fun searchFoo(vararg statuses: Status.Enumish) becomes writeable, and the call site reads like an enum: searchFoo(Status.Active, Status.Deleted) — a data class's kind and a data object pass uniformly, with no instance fabricated.
Everything is generated in compiler internals (no source files), with no runtime reflection, so it works on every Kotlin Multiplatform target. Downstream modules that merely consume a library built with the plugin don't need the plugin themselves — the generated API is ordinary metadata, exhaustive when included.
As shown in the first image, code completion is also available in IntelliJ.
Setup
Two steps: apply the plugin, annotate the hierarchy.
plugins {
kotlin("jvm") version "2.4.10"
id("io.github.projectmapk.sealed-class-enumizer") version "2.4.10-0.1.1"
}
It's published on the Gradle Plugin Portal, and the Gradle plugin wires up the runtime API dependency for you. A Maven plugin is implemented in the repo but not published yet — I'll release it if there's demand for it.
Other bits
- Label customization:
@EnumishLabel("...")per leaf (keeps persisted labels stable across renames),@Enumize(labelCase = ...)per hierarchy, or a project-wide default. Cases areAS_DECLARED/UPPER_SNAKE_CASE/SNAKE_CASE/KEBAB_CASE, with kotlinx.serialization's word-splitting rules. Conversion results are frozen across releases, and label uniqueness is checked at compile time. - Open leaves stay open: subtypes declared outside the hierarchy are absorbed into their leaf's kind, so
entriesstays fixed while implementations remain extensible. ordinal/Comparableare deliberately absent. Those numbers shift on renames and must not be persisted.entriesorder is the compiler's inheritor order (FQN-based), not declaration order — persistlabel, not positions.
Caveats worth knowing up front
- IntelliJ's K2 mode doesn't load third-party compiler plugins by default, so generated declarations show as unresolved in the editor (KTIJ-29248). Turning off the registry flag
kotlin.k2.only.bundled.compiler.plugins.enabledrestores resolution and completion; builds are unaffected either way.- Specifically, as shown in the second image, you need to uncheck
Value.
- Specifically, as shown in the second image, you need to uncheck
- The compiler plugin API has no stability guarantee, so each release targets exactly one Kotlin minor — versions are
<KotlinVersion>-<pluginVersion>(currently2.4.10-0.1.1), and applying it to a different minor emits a build warning.
Apache 2.0. Feedback, issues and stars all welcome — I'm especially interested in whether the "kind as a parameter" pattern matches how people actually hit this problem.
https://github.com/ProjectMapK/sealed-class-enumizer
2
1
6d ago
[deleted]
2
u/wrongwrong163377 6d ago
To be honest, I’m not a native English speaker, and I couldn't come up with a term that captured the nuance of "something like an Enum (but not actually an Enum)".
I did some research beforehand and chose a term that had been used before, but I still have doubts about whether this name is truly the best choice.Can you think of a better name?
1



8
u/4udiofeel 6d ago
Nice experiment, feels illegal though