Question

Match Rust Enum Values with Integers Safely

Question

Given this Rust enum, it is possible to obtain a variant's integer discriminant with a cast:

enum MyEnum {
    A = 1,
    B,
    C,
}

let x = MyEnum::C as i32;

However, matching the integer against enum variants does not compile:

match x {
    MyEnum::A => {}
    MyEnum::B => {}
    MyEnum::C => {}
    _ => {}
}

How can an integer be matched against an enum's discriminant values, or safely converted back into MyEnum? Is there an idiomatic conversion method similar to from that can return Option<MyEnum>?

Short Answer

You will learn why an integer and an enum are different Rust types, how to match integer discriminants correctly, and how to create a safe integer-to-enum conversion with TryFrom. You will also see why directly reinterpreting an arbitrary integer as an enum is unsafe.

Concept

An enum variant such as MyEnum::A is a value whose type is MyEnum. Even when that variant has a numeric discriminant such as 1, it is not itself an i32.

let value: i32 = 1;
let variant: MyEnum = MyEnum::A;

Because these values have different types, Rust cannot use MyEnum::A as a pattern while matching an i32.

A discriminant is the number assigned to a fieldless enum variant. In this example, the values are:

VariantDiscriminant
MyEnum::A1
MyEnum::B2
MyEnum::C3

Casting an enum to an integer is one-way syntax:

let number = MyEnum::C as i32;

Rust does not provide the reverse as MyEnum conversion for arbitrary integers because an integer might not correspond to any valid variant. A safe program must handle invalid values, usually with Result through the standard TryFrom trait, or sometimes with Option in a custom helper method.

Use #[repr(i32)] when the numeric representation is part of an external contract, such as a file format, network protocol, database column, or FFI boundary. It makes the intended integer representation explicit.

Mental Model

Think of an enum as a set of named, approved ID cards:

  • MyEnum::A is the approved card named A.
  • Its printed ID number is 1.
  • An i32 value is just a number someone hands you.

Seeing the number 1 does not automatically turn it into the approved MyEnum::A card. Your code must check whether the number belongs to the approved set and then return the corresponding variant.

Matching an i32 with MyEnum::A is like comparing a raw number to an ID card: they are different kinds of things. Match the number 1, or convert the number into an enum first.

Take Quiz

Syntax and Examples

Declare a fieldless enum with explicit integer representation and implement TryFrom<i32> for safe conversion:

use std::convert::TryFrom;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
enum MyEnum {
    A = 1,
    B,
    C,
}

impl TryFrom<i32> for MyEnum {
    type Error = ();

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        match value {
            1 => Ok(MyEnum::A),
            2 => Ok(MyEnum::B),
            3 => Ok(MyEnum::C),
            _ => Err(()),
        }
    }
}

fn main() {
    let x = MyEnum::C as i32;

    match MyEnum::try_from(x) {
        Ok(MyEnum::A) => println!("A"),
        Ok(MyEnum::B) => println!("B"),
        Ok(MyEnum::C) => println!("C"),
        Err(()) => println!("Unknown value"),
    }
}

The conversion produces Result<MyEnum, ()>:

  • Ok(MyEnum::C) for 3
  • Err(()) for an invalid value such as 99

Once conversion succeeds, the match expression operates on MyEnum, so enum variant patterns work normally.

If you only need to match the integer and do not need an enum value, match integer literals directly:

let x = 2;

match x {
    1 => println!("A"),
    2 => println!("B"),
    3 => println!("C"),
    _ => println!("Unknown value"),
}

Step by Step Execution

Consider this conversion:

let raw_value = 3;
let parsed = MyEnum::try_from(raw_value);

match parsed {
    Ok(MyEnum::A) => println!("First"),
    Ok(MyEnum::B) => println!("Second"),
    Ok(MyEnum::C) => println!("Third"),
    Err(()) => println!("Invalid enum value"),
}

Execution proceeds as follows:

  1. raw_value is an i32 containing 3.
  2. MyEnum::try_from(raw_value) runs the match inside the TryFrom implementation.
  3. The 3 arm matches and returns Ok(MyEnum::C).
  4. parsed now has type Result<MyEnum, ()>.
  5. The outer match checks the result.
  6. Ok(MyEnum::C) matches the third arm, so the program prints Third.

For raw_value = 42, the conversion reaches _ => Err(()), and the program prints Invalid enum value. This prevents invalid input from becoming an invalid enum value.

Real World Use Cases

Integer-to-enum conversion is useful whenever your program receives numeric codes from outside Rust:

  • HTTP or API data: convert numeric status or category codes into meaningful variants.
  • Databases: map an integer column such as role_code into Role::Admin, Role::Editor, or Role::Viewer.
  • Binary files and network protocols: parse documented numeric tags safely.
  • Command-line or configuration input: validate a numeric mode before the rest of the program uses it.
  • Foreign Function Interface (FFI): translate C library constants into a Rust enum after validating the returned value.

In all of these cases, outside input may be unknown, outdated, malformed, or reserved for a future version. A fallible conversion makes that possibility explicit.

Take Quiz

Real Codebase Usage

In production code, keep raw external values at the boundary and convert them as early as possible.

fn handle_mode(raw_mode: i32) -> Result<(), String> {
    let mode = MyEnum::try_from(raw_mode)
        .map_err(|_| format!("unsupported mode: {raw_mode}"))?;

    match mode {
        MyEnum::A => println!("Running mode A"),
        MyEnum::B => println!("Running mode B"),
        MyEnum::C => println!("Running mode C"),
    }

    Ok(())
}

This pattern has useful properties:

  • Validation happens once. The rest of the function receives a valid MyEnum.
  • Invalid data gets a clear error. The raw number is preserved in the message.
  • Enum matching is exhaustive. When a new variant is added, Rust can point out matches that need updating.
  • Named variants improve readability. MyEnum::C communicates intent better than a repeated 3.

For a convenient optional API, a project may add a named helper:

impl MyEnum {
    fn from_i32(value: i32) -> Option<Self> {
        Self::try_from(value).ok()
    }
}

Prefer TryFrom when callers benefit from an error value or when conversion is part of a general Rust API. Use Option when “known or unknown” is all the caller needs.

Common Mistakes

Matching an integer with enum variants

This fails because x is an i32, while MyEnum::A is a MyEnum:

let x: i32 = 1;

match x {
    MyEnum::A => {} // does not compile
    _ => {}
}

Fix it by matching integer literals, or convert x with TryFrom first.

Assuming as works in both directions

This is not a safe reverse conversion:

// let value = 99 as MyEnum; // not allowed

Rust requires you to decide what to do with invalid values. Implement TryFrom<i32> or a from_i32 method.

Using unsafe conversion for unchecked input

Avoid code that reinterprets an arbitrary integer as an enum, such as std::mem::transmute. Not every integer is a valid enum discriminant, and creating an invalid enum value can cause undefined behavior.

Validate first:

let value = MyEnum::try_from(raw_value)?;

Forgetting to update conversion code

If you add a variant, update the conversion mapping too:

enum MyEnum {
    A = 1,
    B,
    C,
    D,
}

Add 4 => Ok(MyEnum::D) to try_from. A direct match on MyEnum is checked for exhaustiveness, but a separate integer mapping needs deliberate maintenance.

Relying on implicit consecutive values as an external format

B and C currently become 2 and 3, but external contracts are clearer when every public code is explicitly documented or assigned. Use #[repr(i32)] when representation matters outside ordinary Rust logic.

Comparisons

ApproachInputOutputBest use
Cast enum to integer: variant as i32Valid enumIntegerSending or storing a known variant as a numeric code
Match integer literalsIntegerBranch selectionSmall local checks where an enum value is unnecessary
TryFrom<i32>IntegerResult<MyEnum, Error>Validating external input with useful error handling
Custom from_i32IntegerOption<MyEnum>Simple “valid or unknown” lookup
Match an enumValid enumBranch selectionBusiness logic after validation

Result versus Option:

  • Choose Result when invalid input should be reported, logged, or propagated with ?.
  • Choose Option when an unknown value is expected and no additional error details are needed.

#[repr(i32)] versus no repr:

  • Without a representation attribute, Rust can still cast a fieldless enum to an integer.
  • With #[repr(i32)], you explicitly state that the enum uses i32 representation, which is especially useful for FFI and persisted numeric values.
Take Quiz

Cheat Sheet

use std::convert::TryFrom;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
enum Status {
    Ready = 1,
    Running = 2,
    Done = 3,
}

impl TryFrom<i32> for Status {
    type Error = ();

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        match value {
            1 => Ok(Self::Ready),
            2 => Ok(Self::Running),
            3 => Ok(Self::Done),
            _ => Err(()),
        }
    }
}
  • Convert enum to integer: let code = Status::Ready as i32;
  • Convert integer safely: let status = Status::try_from(code);
  • Match raw integers with 1, 2, and 3, not Status::Ready.
  • Match enum variants only after you have a Status value.
  • Handle unknown input: Err(_) or .ok().
  • Do not use transmute to turn unchecked integers into enums.
  • Use #[repr(i32)] when numeric representation is part of an external interface.

FAQ

Can Rust match an i32 against an enum variant?

No. An i32 and an enum variant have different types. Match integer literals, or convert the integer to the enum first.

How do I convert an integer to an enum in Rust?

Implement TryFrom<i32> and return Ok(variant) for known discriminants and Err(...) for unknown values.

Why is TryFrom better than an unchecked cast?

An integer may not identify a real variant. TryFrom validates the value instead of allowing an invalid enum value to be created.

Can I return Option<MyEnum> instead of Result<MyEnum, _>?

Yes. A custom from_i32 method can return Option<Self>. Internally, it can call Self::try_from(value).ok().

Do I need #[repr(i32)]?

It is recommended when the integer representation matters for FFI, a protocol, storage, or another external contract. It also documents your intent clearly.

Can I write MyEnum::A as i32 directly in a match arm?

Do not rely on casts as patterns. The clearest approaches are matching numeric literals or constants, or converting to MyEnum before matching.

What happens if a new enum variant is added?

Update the TryFrom mapping. Matching on a MyEnum afterward remains valuable because Rust can require all variants to be handled.

Take Quiz

Mini Project

Description

Build a parser for numeric task-state codes received from a small external system. The program converts each raw integer into a typed Rust enum, prints valid states, and reports invalid codes without crashing.

Goal

Safely translate raw i32 task-state codes into enum variants and handle unknown values.

Requirements

  1. Define a TaskState enum with numeric codes 1, 2, and 3.
  2. Use #[repr(i32)] and derive Debug, Clone, Copy, PartialEq, and Eq.
  3. Implement TryFrom<i32> for TaskState.
  4. Process a list containing both valid and invalid integer codes.
  5. Print a useful message for every code.
Take Quiz

Keep learning

Related questions

View more