Question

Kotlin Enum Reverse Lookup: Map Values Back to Enum Constants

kotlinenums

Question

How can I perform a reverse lookup on a Kotlin enum, finding an enum constant from its associated integer value?

For example, this enum builds a map in its companion object:

enum class Type(val value: Int) {
    A(1),
    B(2),
    C(3);

    companion object {
        val map: MutableMap<Int, Type> = HashMap()

        init {
            for (type in Type.values()) {
                map[type.value] = type
            }
        }

        fun fromInt(value: Int?): Type? {
            return map[value]
        }
    }
}

Is a companion-object map the best approach? How can this pattern be improved in Kotlin, and can similar reverse-lookup logic be reused across several enums?

Short Answer

You will learn how to convert external values, such as database IDs or API codes, back into Kotlin enum constants. The page covers concise lookup maps, the modern entries API, handling missing and duplicate values, performance trade-offs, and realistic ways to share the pattern between enums.

Concept

An enum constant can carry extra data:

enum class Type(val value: Int) {
    A(1), B(2), C(3)
}

This supports the forward direction easily: Type.A.value is 1.

A reverse lookup performs the opposite conversion: given 1, return Type.A. This is common when an application receives compact codes from a database, configuration file, URL parameter, or API.

A lookup map is usually the right choice when reverse lookups happen repeatedly. It is built once from the enum constants, then each lookup is typically constant time:

private val byValue = entries.associateBy { it.value }

The map is a Map<Int, Type>, where each integer key points to its matching enum value. Prefer a read-only Map rather than exposing a mutable map: callers should be able to look up values, not alter the enum's lookup rules.

For a tiny enum used only occasionally, a simple search is also valid. The best choice depends on how often the reverse lookup runs and whether clarity or repeated-lookup performance matters most.

Mental Model

Think of enum constants as labeled drawers:

  • Type.A is a drawer.
  • A(1) puts the number 1 on its label.
  • A reverse lookup asks: “Which drawer has label 1?”

Searching with firstOrNull means checking drawer labels one at a time. A map is an index card at the front of the cabinet that says 1 → A, 2 → B, and 3 → C. Creating the index takes a little setup once, but finding a drawer afterward is fast.

Take Quiz

Syntax and Examples

A concise Kotlin implementation uses an immutable lookup map in the companion object:

enum class Type(val value: Int) {
    A(1),
    B(2),
    C(3);

    companion object {
        private val byValue: Map<Int, Type> = entries.associateBy { it.value }

        fun fromIntOrNull(value: Int): Type? = byValue[value]
    }
}

fun main() {
    println(Type.fromIntOrNull(2)) // B
    println(Type.fromIntOrNull(99)) // null
}

entries provides the enum constants as a collection. associateBy { it.value } creates a map whose keys are the integer values and whose values are the enum constants.

If a missing code should be an error rather than null, use getValue:

fun fromInt(value: Int): Type = byValue.getValue(value)

getValue throws NoSuchElementException if the code is absent. This is useful only when an unknown value indicates invalid program data or a validation failure.

For Kotlin versions before entries is available, use values():

private val byValue = values().associateBy { it.value }

For a very small enum and infrequent lookups, avoid a map altogether:

fun fromIntOrNull(value: Int): Type? = entries.firstOrNull { it.value == value }

This reads clearly, but checks constants sequentially on every call.

Step by Step Execution

Consider this code:

enum class Status(val code: Int) {
    NEW(10),
    ACTIVE(20),
    CLOSED(30);

    companion object {
        private val byCode = entries.associateBy { it.code }

        fun fromCodeOrNull(code: Int): Status? = byCode[code]
    }
}

val status = Status.fromCodeOrNull(20)

Execution proceeds as follows:

  1. Kotlin initializes the Status enum constants: NEW, ACTIVE, and CLOSED.

  2. The companion object's byCode property is created once.

  3. entries.associateBy { it.code } produces a map equivalent to:

    mapOf(
        10 to Status.NEW,
        20 to Status.ACTIVE,
        30 to Status.CLOSED
    )
    
  4. fromCodeOrNull(20) reads byCode[20].

  5. The map finds key 20 and returns Status.ACTIVE.

  6. status therefore contains Status.ACTIVE.

If the call were Status.fromCodeOrNull(25), no map key would match and the result would be null.

Real World Use Cases

Reverse enum lookup is useful whenever a system exchanges stable codes rather than enum names.

  • Database columns: Convert an integer such as 2 into OrderStatus.SHIPPED after reading a row.
  • API payloads: Interpret a JSON code such as "priority": 3 as Priority.HIGH.
  • Legacy integrations: Map codes required by an older service to readable domain names in Kotlin.
  • Configuration: Convert a numeric or string setting into a supported application mode.
  • Protocol messages: Decode compact wire-format values into meaningful enum constants.

For persisted or public values, treat the associated value as part of a contract. Changing ACTIVE(20) to ACTIVE(21) can break existing data or API clients, even though the enum constant name has not changed.

Take Quiz

Real Codebase Usage

In production code, keep the lookup details private and expose an intentional API:

enum class PaymentState(val code: Int) {
    PENDING(0), PAID(1), FAILED(2);

    companion object {
        private val byCode = entries.associateBy(PaymentState::code)

        fun fromCodeOrNull(code: Int): PaymentState? = byCode[code]

        fun requireFromCode(code: Int): PaymentState =
            byCode[code] ?: throw IllegalArgumentException("Unknown payment state code: $code")
    }
}

Common patterns include:

  • Nullable parser: fromCodeOrNull lets the caller decide how to handle unknown input.
  • Validation boundary: requireFromCode is useful when invalid input must immediately fail with a clear message.
  • Defaulting at the edge: Apply defaults where input enters the system, not silently inside every lookup.
val state = PaymentState.fromCodeOrNull(apiCode) ?: PaymentState.PENDING
  • Private map: Keep byCode private so no other code can mutate or depend on its implementation.
  • Tests for the contract: Test valid codes, unknown codes, and duplicate-code detection if codes are externally important.

Kotlin cannot directly require every enum companion object to implement the same generic static-like function. Each enum has its own companion object. In practice, each enum defines a small, explicit lookup function, while shared validation or lookup helpers can be ordinary generic functions.

Common Mistakes

Exposing a mutable lookup map

Avoid giving callers a MutableMap:

// Avoid
val map: MutableMap<Int, Type> = HashMap()

Any caller could run Type.map[1] = Type.C, making lookups incorrect. Prefer a private read-only map:

private val byValue: Map<Int, Type> = entries.associateBy { it.value }

Accepting a nullable value unnecessarily

A map can accept a nullable key, but Int? often hides uncertainty in the caller. Prefer this:

fun fromIntOrNull(value: Int): Type? = byValue[value]

Handle a nullable external value before calling it:

val type = inputCode?.let(Type::fromIntOrNull)

Ignoring duplicate values

associateBy keeps the last enum constant when keys repeat:

enum class BadType(val value: Int) {
    A(1),
    B(1)
}

val byValue = BadType.entries.associateBy { it.value }
// Key 1 maps to B; A has been overwritten.

Codes intended for reverse lookup must be unique. Add a check when that guarantee matters:

private val byValue: Map<Int, Type> = entries
    .groupBy { it.value }
    .also { groups ->
        require(groups.values.all { it.size == 1 }) {
            "Type values must be unique"
        }
    }
    .mapValues { (_, types) -> types.single() }

Using ordinal as a stored code

ordinal is the position of a constant, starting at zero. Reordering constants changes it:

enum class Type { A, B, C }
// Type.B.ordinal is 1

Do not store or transmit ordinals. Use explicit stable values such as A(100).

Using valueOf for custom integer values

valueOf looks up an enum by its constant name, not its associated property:

Type.valueOf("A") // works
// Type.valueOf("1") // throws IllegalArgumentException

Use your fromIntOrNull function for custom codes.

Comparisons

ApproachExampleLookup costBest for
Sequential searchentries.firstOrNull { it.value == code }Linear per callTiny enums and rare lookups
Precomputed mapentries.associateBy { it.value }Usually constant per callRepeated lookups, APIs, databases
valueOfType.valueOf("A")Built-in name lookupInput that contains enum names
ordinalType.A.ordinalDirect positionInternal temporary logic only; not persisted codes

A map is more efficient for frequent reverse lookups, while firstOrNull is often simpler for a small enum. Both are correct when used in the appropriate context.

enumValues<Type>() and Type.values() create an array representation of enum constants. Modern Kotlin code can use Type.entries, which provides an enum-entry collection and avoids relying on the older values() API.

Take Quiz

Cheat Sheet

enum class Type(val code: Int) {
    A(1), B(2), C(3);

    companion object {
        private val byCode = entries.associateBy(Type::code)

        fun fromCodeOrNull(code: Int): Type? = byCode[code]
        fun requireFromCode(code: Int): Type = byCode.getValue(code)
    }
}
  • Use entries.associateBy { it.code } to build a reverse-lookup map.
  • Keep the map private and typed as Map, not MutableMap.
  • Return null for expected unknown input: byCode[code].
  • Throw for invalid required input: byCode.getValue(code) or a custom exception.
  • Ensure lookup codes are unique; duplicate keys cause associateBy to retain the last value.
  • Use explicit properties for stored or transmitted codes; never persist ordinal.
  • Use entries.firstOrNull { ... } for simple, infrequent lookups.
  • On older Kotlin versions, replace entries with values().

FAQ

What is reverse lookup in a Kotlin enum?

It means finding an enum constant from one of its associated values, such as finding Type.B from integer code 2.

Should I use entries or values() for Kotlin enums?

Use entries in modern Kotlin. It is the newer enum-entry API. Use values() when supporting an older Kotlin version where entries is unavailable.

Is an enum lookup map initialized every time I call fromCodeOrNull?

No. A companion object property is initialized once for the enum class, then reused by later calls.

What happens if two enum constants use the same code?

With associateBy, the later constant replaces the earlier one for that key. Validate uniqueness when duplicate codes would be a bug.

Should an enum lookup function return null or throw an exception?

Return null when an unknown external value is expected and can be handled. Throw when an unknown value violates a required input contract.

Can Kotlin share one generic reverse-lookup companion object across all enums?

Not directly in the same static-like way, because each enum owns a separate companion object. You can share helper functions, but each enum usually exposes its own small fromCodeOrNull function.

Why should I not use ordinal as an external enum code?

Its value changes if constants are inserted or reordered. Explicit codes remain stable and are safer for databases and APIs.

Take Quiz

Mini Project

Description

Build a ShippingMethod enum for an order-processing system. The system receives integer method codes from an external checkout service and must convert them into readable enum values safely.

Goal

Create a reverse lookup that returns the shipping method for valid codes and rejects unknown codes with a clear error.

Requirements

  • Create a ShippingMethod enum with three explicit integer codes.
  • Build a private lookup map from code to enum constant.
  • Provide a nullable lookup function for optional input.
  • Provide a required lookup function with a helpful error message.
  • Demonstrate one valid lookup and one unknown-code case.
Take Quiz

Keep learning

Related questions

View more