Enum in C#: Declare, Convert, Flags and JSON (.NET 10)

Status code 3 means nothing to whoever reads your code next year. Shipped means everything, and the compiler can check it for you.

Quick answer: An enum in C# is a value type that gives names to a fixed set of integer constants. Declare it with enum OrderStatus { Pending, Paid, Shipped }, cast it with (int), and turn text into it with Enum.TryParse.

enum OrderStatus { Pending, Paid, Shipped }

OrderStatus status = OrderStatus.Paid;
Console.WriteLine(status);       // Paid
Console.WriteLine((int)status);  // 1

In this tutorial, I’ll walk you through the enum in C# the way my team uses it in .NET 10 APIs: declaring, casting, parsing, looping, flags, switch expressions and JSON. One small online-store sample runs through every step.

What You Need Before You Start with Enums in C#

  • The .NET 10 SDK. Every example here is a file-based app you run with dotnet run File.cs, no project file needed.
  • VS Code with the C# Dev Kit, or any editor. If you’re new to it, see how to create a console application in Visual Studio Code.
  • The sample: an online store with an OrderStatus enum, a ShippingRegion enum (West, Midwest, South, Northeast) and a Permissions flags enum for staff accounts.

Step 1: Declare an Enum in C# with Explicit Values

Here’s the store’s OrderStatus enum. I gave it a byte underlying type and jumped Shipped to 5, so you can see both the default numbering and explicit values:

OrderStatus status = OrderStatus.Shipped;
Console.WriteLine(status);
Console.WriteLine((int)status);
Console.WriteLine((int)OrderStatus.Delivered);

ShippingRegion region = ShippingRegion.Midwest;
Console.WriteLine($"{region} = {(int)region}");
Console.WriteLine(Enum.GetUnderlyingType(typeof(OrderStatus)));

enum OrderStatus : byte { None, Pending, Paid, Shipped = 5, Delivered }
enum ShippingRegion { West, Midwest, South, Northeast }

Output:

Shipped
5
6
Midwest = 1
System.Byte
Enum in C# declared with a byte underlying type and explicit value Shipped = 5, running in VS Code with dotnet run
Shipped is 5 because I set it; Delivered follows as 6

Members start at 0 and count up by 1, and an explicit value resets the count, so Delivered becomes 6. ShippingRegion has no type, so it’s int and Midwest is 1.

The underlying type can be any integral type: byte, short, int, long and their unsigned or signed partners. My guide to data types in C# lists their ranges.

Step 2: Cast an Enum to int and an int Back to an Enum

Conversions between an enum and its number are explicit, so you need a cast both ways. This is what I do when a database column stores the status as a number:

OrderStatus paid = OrderStatus.Paid;
int code = (int)paid;
Console.WriteLine($"{paid} -> {code}");

OrderStatus fromDb = (OrderStatus)5;
Console.WriteLine($"5 -> {fromDb}");

OrderStatus odd = (OrderStatus)42;
Console.WriteLine($"42 -> {odd}, defined: {Enum.IsDefined(odd)}");

OrderStatus blank = default;
Console.WriteLine($"default -> {blank}");

enum OrderStatus : byte { None, Pending, Paid, Shipped = 5, Delivered }

Output:

Paid -> 2
5 -> Shipped
42 -> 42, defined: False
default -> None

Look at the third line. Casting 42 doesn’t fail: you get an OrderStatus that holds 42, and ToString() prints the number. That’s why I always run Enum.IsDefined on numbers that come from outside.

The default value of any enum is 0, even when no member has that value, which is a good reason to keep a None = 0 member. For casting rules in general, see type casting in C#.

Step 3: Convert a String to an Enum with Enum.TryParse

Form fields, query strings and CSV files hand you text, not enums. Enum.TryParse<TEnum> converts it without throwing, and the ignoreCase: true overload accepts “shipped” as well as “Shipped”:

string[] inputs = ["Shipped", "shipped", "Lost", "42", "2"];

foreach (string input in inputs)
{
    bool ok = Enum.TryParse(input, ignoreCase: true, out OrderStatus s);
    bool real = ok && Enum.IsDefined(s);
    Console.WriteLine($"{input,-8} ok={ok,-5} -> {s,-8} defined={real}");
}

enum OrderStatus : byte { None, Pending, Paid, Shipped = 5, Delivered }

Output:

Shipped  ok=True  -> Shipped  defined=True
shipped  ok=True  -> Shipped  defined=True
Lost     ok=False -> None     defined=False
42       ok=True  -> 42       defined=False
2        ok=True  -> Paid     defined=True
C# Enum.TryParse with ignoreCase true accepting the undefined number 42, and Enum.IsDefined returning False, in VS Code
TryParse says yes to “42”; only IsDefined catches it

The “42” row surprises most people. TryParse accepts any numeric string, defined or not, and returns true. “Lost” isn’t a member name, so it fails and s falls back to 0, which prints as None.

Enum.Parse Throws When the Name Doesn’t Exist

Enum.Parse<OrderStatus> does the same job but throws an ArgumentException on bad input. I only use it for values I control, such as config files, and keep a try-catch block in C# around it:

try
{
    OrderStatus s = Enum.Parse<OrderStatus>("Lost");
    Console.WriteLine(s);
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message);
}

enum OrderStatus : byte { None, Pending, Paid, Shipped = 5, Delivered }

Output:

Requested value 'Lost' was not found.

Step 4: Convert an Enum to a String with ToString and nameof

Same OrderStatus, now going the other way. ToString() gives the name, and the format strings give the number:

OrderStatus s = OrderStatus.Delivered;

Console.WriteLine(s.ToString());
Console.WriteLine(s.ToString("D"));
Console.WriteLine(s.ToString("X"));
Console.WriteLine(Enum.GetName(s));
Console.WriteLine(nameof(OrderStatus.Delivered));

enum OrderStatus : byte { None, Pending, Paid, Shipped = 5, Delivered }

Output:

Delivered
6
06
Delivered
Delivered

"D" prints the decimal value and "X" prints hex padded to the type’s size, so a byte shows 06. More format strings are in my ToString in C# guide.

nameof(OrderStatus.Delivered) is resolved at compile time, so it costs nothing at run time and survives a rename in your IDE. I use it in log messages and exception text.

Step 5: Loop Through Enum Values with Enum.GetValues<T>()

To fill a drop-down or print a report, loop over the members. The generic Enum.GetValues<T>() and Enum.GetNames<T>() (.NET 5 and later) return typed arrays you can drop straight into a foreach loop in C#:

foreach (OrderStatus s in Enum.GetValues<OrderStatus>())
    Console.WriteLine($"{(int)s}: {s}");

string[] names = Enum.GetNames<ShippingRegion>();
Console.WriteLine(string.Join(", ", names));
Console.WriteLine($"Regions: {names.Length}");

enum OrderStatus : byte { None, Pending, Paid, Shipped = 5, Delivered }
enum ShippingRegion { West, Midwest, South, Northeast }

Output:

0: None
1: Pending
2: Paid
5: Shipped
6: Delivered
West, Midwest, South, Northeast
Regions: 4

Values come back sorted by their number, not by declaration order. On older code you’ll see Enum.GetValues(typeof(OrderStatus)), which returns an untyped Array and needs a cast.

Step 6: Combine Options with a [Flags] Enum in C#

Plain enums hold one choice. When a staff account can have several permissions at once, I use a [Flags] enum whose values are powers of two, so each member owns one bit:

var jenna = Permissions.Read | Permissions.Write;
Console.WriteLine($"Jenna: {jenna} ({(int)jenna})");
Console.WriteLine($"Can write: {jenna.HasFlag(Permissions.Write)}");
Console.WriteLine($"Can delete: {(jenna & Permissions.Delete) != 0}");

jenna |= Permissions.Delete;
jenna &= ~Permissions.Write;
Console.WriteLine($"After change: {jenna}");
Console.WriteLine($"All: {Permissions.All} ({(int)Permissions.All})");

[Flags]
enum Permissions
{
    None = 0, Read = 1, Write = 2, Delete = 4, Approve = 8,
    All = Read | Write | Delete | Approve
}

Output:

Jenna: Read, Write (3)
Can write: True
Can delete: False
After change: Read, Delete
All: All (15)
C# [Flags] enum Permissions combined with the | operator, checked with HasFlag and the & operator, and changed with |= and &= ~ in VS Code
Read | Write prints as “Read, Write” because of [Flags]

| adds a flag, & ~ removes one, and HasFlag or (x & flag) != 0 tests one. The [Flags] attribute is what makes ToString() print the names separated by commas.

Step 7: Use an Enum in a C# Switch Expression

Enums and switch expressions belong together. Same ShippingRegion enum, now mapped to delivery days with an or pattern; my switch case in C# tutorial covers the other pattern types:

foreach (var region in Enum.GetValues<ShippingRegion>())
    Console.WriteLine($"{region}: {DeliveryDays(region)} days");

static int DeliveryDays(ShippingRegion region) => region switch
{
    ShippingRegion.West => 2,
    ShippingRegion.Midwest => 3,
    ShippingRegion.South or ShippingRegion.Northeast => 4,
    _ => throw new ArgumentOutOfRangeException(nameof(region))
};

enum ShippingRegion { West, Midwest, South, Northeast }

Output:

West: 2 days
Midwest: 3 days
South: 4 days
Northeast: 4 days
C# switch expression on a ShippingRegion enum with an or pattern for South and Northeast and a discard arm that throws, output in VS Code
Each region maps to its delivery days

Keep the _ arm even when you’ve listed every member. Without it, the compiler warns with CS8524, because a cast like (ShippingRegion)9 can still reach the switch.

Step 8: Serialize an Enum as a String with System.Text.Json

By default System.Text.Json writes enums as numbers. For an API, I’d rather send names that clients can read, so I add JsonStringEnumConverter:

#:property PublishAot=false
using System.Text.Json;
using System.Text.Json.Serialization;

var order = new Order(1042, "Ryan Carter", OrderStatus.Shipped);
Console.WriteLine(JsonSerializer.Serialize(order));

var opts = new JsonSerializerOptions();
opts.Converters.Add(new JsonStringEnumConverter());
Console.WriteLine(JsonSerializer.Serialize(order, opts));

string body = """{"Id":1043,"Customer":"Chloe Brady","Status":"paid"}""";
Console.WriteLine(JsonSerializer.Deserialize<Order>(body, opts));

record Order(int Id, string Customer, OrderStatus Status);
enum OrderStatus : byte { None, Pending, Paid, Shipped = 5, Delivered }

Output:

{"Id":1042,"Customer":"Ryan Carter","Status":5}
{"Id":1042,"Customer":"Ryan Carter","Status":"Shipped"}
Order { Id = 1043, Customer = Chloe Brady, Status = Paid }
System.Text.Json serializing a C# enum as the number 5 by default and as "Shipped" with JsonStringEnumConverter, in VS Code
Same order, before and after JsonStringEnumConverter

Note: file-based apps turn on Native AOT by default, which disables reflection-based JSON, so the #:property PublishAot=false line switches it back on for this demo.

The converter reads names without caring about case, which is why “paid” came back as Paid. In an ASP.NET Core minimal API, add the same converter in ConfigureHttpJsonOptions so every endpoint uses it.

For trimmed or Native AOT apps, put [JsonConverter(typeof(JsonStringEnumConverter<OrderStatus>))] on the enum instead. If the JSON output itself looks wrong, my post on how to convert an object to JSON without escape characters helps.

Step 9: Show Friendly Enum Display Names with [Description]

Member names can’t contain spaces, but a UI often needs “West Coast”. I put a [Description] attribute on the members and read it with reflection, falling back to the name:

using System.ComponentModel;
using System.Reflection;

foreach (var r in Enum.GetValues<ShippingRegion>())
    Console.WriteLine($"{r} -> {Label(r)}");

static string Label(ShippingRegion r) =>
    typeof(ShippingRegion).GetField(r.ToString())?
        .GetCustomAttribute<DescriptionAttribute>()?.Description
    ?? r.ToString();

enum ShippingRegion
{
    [Description("West Coast")] West,
    [Description("Midwest (IL, OH, MI)")] Midwest,
    South,
    [Description("Northeast Corridor")] Northeast
}

Output:

West -> West Coast
Midwest -> Midwest (IL, OH, MI)
South -> South
Northeast -> Northeast Corridor

South has no attribute, so it falls back to its name. In Blazor or MVC, [Display(Name = "...")] works the same way. For labels that change often, I’d load them into a dictionary in C# instead.

C# Enum Cheat Sheet

Here’s every task from the steps above, with the result each one printed:

TaskCodeResult
Enum to int(int)OrderStatus.Shipped5
int to enum(OrderStatus)5Shipped
Check a numberEnum.IsDefined((OrderStatus)42)False
String to enumEnum.TryParse("shipped", true, out OrderStatus s)True, Shipped
Enum to strings.ToString() / nameof(...)Delivered
All valuesEnum.GetValues<OrderStatus>()None … Delivered
Test a flagjenna.HasFlag(Permissions.Write)True
JSON as textnew JsonStringEnumConverter()“Status”:”Shipped”

Team Rules for Naming Enums in C#

These are the enum rules in our code reviews, and they follow Microsoft’s guidelines. The full list is in my post on C# enum naming conventions:

  • Singular PascalCase for normal enums (OrderStatus), plural for flags (Permissions).
  • No Enum suffix and no type prefix on members (Shipped, not OrderStatusShipped).
  • Add None = 0 so default means something.
  • Never renumber a member that’s already stored in a database or sent over an API.

If a value is a single fixed number rather than a set of choices, use a const instead, named as in my C# constant naming conventions guide.

Common Mistakes to Avoid with Enums in C#

MistakeWhat happensFix
Trusting TryParse alone“42” returns true with an undefined valueAlso call Enum.IsDefined
No member equal to 0default is a value with no nameAdd None = 0
[Flags] values 1, 2, 33 overlaps Read | WriteUse powers of two: 1, 2, 4, 8
Dropping the _ armCS8524 warning, then SwitchExpressionException at run timeAdd a _ arm that throws
Renaming a memberStored JSON strings stop parsingKeep the old name or map it

Can an Enum Have Methods in C#?

No, you can’t declare a method inside an enum. You can add behavior from the outside, though, and C# 14 extension members let that be a property instead of a method:

foreach (var s in Enum.GetValues<OrderStatus>())
    Console.WriteLine($"{s}: open = {s.IsOpen}");

static class OrderStatusExtensions
{
    extension(OrderStatus status)
    {
        public bool IsOpen => status is OrderStatus.Pending
            or OrderStatus.Paid or OrderStatus.Shipped;
    }
}

enum OrderStatus : byte { None, Pending, Paid, Shipped = 5, Delivered }

Output:

None: open = False
Pending: open = True
Paid: open = True
Shipped: open = True
Delivered: open = False

Older code uses a static extension method like IsOpen(this OrderStatus s), which still works. The new extension block is covered in what is new in C# 14.

Next Steps

Enums are value types, just like structs. Once you’re comfortable with them, read my structure in C# guide to see when a small struct is the better model for your data.

A few more tutorials that pair well with this one:

Frequently Asked Questions

What is the default underlying type of an enum in C#?

int. The first member is 0 and each next one adds 1 unless you set a value. You can pick any other integral type with a colon, for example enum OrderStatus : byte.

How do I convert a string to an enum in C#?

Use Enum.TryParse<TEnum>(text, ignoreCase: true, out var value). It returns false for unknown names, but it accepts any number, so also check Enum.IsDefined(value).

How do I get the int value of an enum?

Cast it: (int)OrderStatus.Shipped. For a non-int underlying type, cast to that type or to a wider one. ToString("D") gives the same number as text.

When should I use a [Flags] enum?

When one variable can hold several choices at once, like permissions or days of the week. Give each member a power of two and combine them with |. See Enumeration types on Microsoft Learn.

Is an enum a value type in C#?

Yes. Every enum derives from System.Enum and is a value type, so it’s copied on assignment and can’t be null unless you declare it as OrderStatus?.

Why does Enum.TryParse return true for a number that isn’t defined?

That’s documented behavior: a numeric string is converted to the underlying type even when no member has that value. The Enum.TryParse reference recommends calling IsDefined.