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.Ais a drawer.A(1)puts the number1on 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.
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:
-
Kotlin initializes the
Statusenum constants:NEW,ACTIVE, andCLOSED. -
The companion object's
byCodeproperty is created once. -
entries.associateBy { it.code }produces a map equivalent to:mapOf( 10 to Status.NEW, 20 to Status.ACTIVE, 30 to Status.CLOSED ) -
fromCodeOrNull(20)readsbyCode[20]. -
The map finds key
20and returnsStatus.ACTIVE. -
statustherefore containsStatus.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
2intoOrderStatus.SHIPPEDafter reading a row. - API payloads: Interpret a JSON code such as
"priority": 3asPriority.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.
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:
fromCodeOrNulllets the caller decide how to handle unknown input. - Validation boundary:
requireFromCodeis 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
byCodeprivate 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
| Approach | Example | Lookup cost | Best for |
|---|---|---|---|
| Sequential search | entries.firstOrNull { it.value == code } | Linear per call | Tiny enums and rare lookups |
| Precomputed map | entries.associateBy { it.value } | Usually constant per call | Repeated lookups, APIs, databases |
valueOf | Type.valueOf("A") | Built-in name lookup | Input that contains enum names |
ordinal | Type.A.ordinal | Direct position | Internal 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.
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
privateand typed asMap, notMutableMap. - Return
nullfor 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
associateByto 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
entrieswithvalues().
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.
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
ShippingMethodenum 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.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Add a Maven Repository URL in Gradle Kotlin DSL (build.gradle.kts)
Learn how to add Maven Central and a custom Maven repository URL in Gradle Kotlin DSL using build.gradle.kts syntax.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.