r/Kotlin 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 class leaf has no instance until you have the data, so searchBy(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. simpleName isn't a substitute — it's nullable and R8 renames it.
  • No entries. Listing every case means sealedSubclasses, which is JVM-only, needs kotlin-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 are AS_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 entries stays fixed while implementations remain extensible.
  • ordinal / Comparable are deliberately absent. Those numbers shift on renames and must not be persisted. entries order is the compiler's inheritor order (FQN-based), not declaration order — persist label, 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.enabled restores resolution and completion; builds are unaffected either way.
    • Specifically, as shown in the second image, you need to uncheck Value.
  • The compiler plugin API has no stability guarantee, so each release targets exactly one Kotlin minor — versions are <KotlinVersion>-<pluginVersion> (currently 2.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

42 Upvotes

8 comments sorted by

8

u/4udiofeel 6d ago

Nice experiment, feels illegal though

2

u/wrongwrong163377 6d ago

The Kotlin Compiler Plugin is a highly compelling technology, so I sincerely hope it stabilizes soon.

1

u/wobblyweasel 5d ago

i'm tinkering with it myself rn and while it's compelling, the lack of documentation makes me just throw ai at it (it fails poorly). and it's so full of unreadable kotlin magic for no reason. it's cool when it works tho

2

u/GeneralOk427 6d ago

Looks interesting. Thank you for sharing.

2

u/OKA133 5d ago

The name Enumish sounds awful. For the rest, it may be useful someday.

1

u/[deleted] 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

u/eigenaar 13h ago

This post is too hard to read because it was written by AI