Back to Blog
C#

C# Enum Custom Values: How to Define and Use Them

Learn how to define C# enums with custom numeric values, parse them, handle duplicate values, and use them with serialization and switch statements.

enumC#underlying typeparsingserialization
Illustration of a C# enum with custom numeric values mapped to named members, showing the relationship between names and underlying integers.

When you define an enum in C#, each member gets an underlying integer value automatically, starting from zero and incrementing by one. That default is often sufficient, but there are cases where you need explicit control over the numeric values. For example, when an enum maps to external data such as a database column, a wire protocol, or a legacy API, the numbers must match the contract exactly. This article explains how to assign custom values to C# enums, how those values behave at runtime, and where the design can trip you up if you are not careful.

Defining an Enum with Custom Values

The syntax for assigning custom values is straightforward: each member can be initialized with a constant expression that is convertible to the enum's underlying type. That type is int by default, and you can declare it as byte, sbyte, short, ushort, int, uint, long, or ulong.

public enum HttpStatusCode { Continue = 100, Ok = 200, Created = 201, Accepted = 202, BadRequest = 400, Unauthorized = 401, NotFound = 404, InternalServerError = 500 }

You can also let some members rely on the implicit increment after an explicit value. For instance, if Ok = 200, the next member without an explicit value gets 201. That behavior can reduce repetition when you have a sequential range, but it can also hide mistakes if the sequence is not intentional.

public enum ErrorCode { None = 0, General = 1, Validation = 2, Auth = 3, // Next member would be 4 if not specified }

Why Custom Values Matter

Default enum values are fragile when the numeric value is part of a public contract. If you insert a new member in the middle of an enum that is stored in a database, all subsequent members shift unless you explicitly assign values. That can corrupt persisted data or break an API that sends the integer over the wire. Assigning explicit custom values makes the mapping stable and self-documenting.

Another common reason is interoperability. Many network protocols and file formats use fixed numeric codes. Using an enum with matching values makes the code more readable than scattering magic numbers throughout the application. It also gives you compile-time type safety when you work with those codes.

Parsing and Formatting with Custom Values

Enum.Parse and Enum.TryParse accept either a member name or a string representation of the underlying numeric value. If the string is numeric, it is converted to that value and the method returns an enum with that underlying value. If the value is defined by a named member, that member is returned.

string input = "200"; if (Enum.TryParse<HttpStatusCode>(input, out var status)) { Console.WriteLine(status); // Output: Ok }

A numeric string does not have to match a named member. Enum.TryParse<HttpStatusCode>("999", out _) returns true and produces an enum whose underlying value is 999. If your input can contain arbitrary numbers, test for that case after parsing.

Be aware that Enum.Parse is case-sensitive by default. If you need case-insensitive matching, pass true for the ignoreCase parameter. The numeric string is parsed as the underlying type, so a value like 200 works for an int-based enum, but 200L would not parse for an int underlying type.

Formatting works the other way. Calling ToString() on an enum member returns its name, not its numeric value. To get the numeric value, cast the enum to its underlying type.

HttpStatusCode status = HttpStatusCode.NotFound; Console.WriteLine(status.ToString()); // NotFound Console.WriteLine((int)status); // 404

Handling Duplicate Values and Aliases

C# allows multiple enum members to share the same numeric value. This is often used to provide aliases for the same logical state. For example, a Success alias might equal Ok.

public enum Result { Success = 1, Ok = 1, Failure = 2 }

When you parse a numeric string that matches multiple members, the runtime returns a value with that underlying number, but the name reported by ToString() is not guaranteed to be deterministic. Do not rely on which alias is chosen. If you need a canonical member, avoid aliases or handle the mapping explicitly.

Duplicate values also affect Enum.GetValues and Enum.GetNames. GetValues returns enum values, and those values carry only their underlying number; they do not preserve which alias was used. If you need unique numeric codes, apply Distinct() to the result. If you need the member names, including aliases, use Enum.GetNames.

On .NET 5 and later, you can use the generic overloads Enum.GetValues<TEnum>() and Enum.GetNames<TEnum>(). On earlier frameworks, pass typeof(Result) to the non-generic versions.

Underlying Types and Range Considerations

The default underlying type is int, but you can change it to a smaller type to save memory when the enum is stored in an array or a struct. The syntax is simple:

public enum ByteCode : byte { Zero = 0, One = 1, Max = byte.MaxValue }

Changing the underlying type affects the range of allowed values. If you assign a constant that does not fit, the compiler raises an error. For example, assigning 256 to a byte-based enum is a compile-time error. This is a useful guard against accidental out-of-range values.

The underlying type also affects how the enum is serialized. In System.Text.Json, the default enum converter writes the numeric value, so changing the underlying type from int to long changes the JSON output. For external contracts, keep the underlying type stable to avoid breaking changes.

Using Custom Values in Switch Statements

Switch statements on enums are a common pattern. With custom values, the compiler checks that each case label is a valid constant for the enum. You can use the member names directly, which is more readable than comparing against raw integers.

switch (status) { case HttpStatusCode.Ok: Console.WriteLine("Success"); break; case HttpStatusCode.NotFound: Console.WriteLine("Missing"); break; default: Console.WriteLine("Other"); break; }

A subtle issue arises when you have duplicate values. If you write two case labels that map to the same numeric value, the compiler will reject the switch because the cases are not distinct. For example, if Success and Ok both equal 1, you cannot have both as separate case labels. You must choose one or handle the alias explicitly.

Serialization and Database Mapping

When an enum is used in a database, you often store the numeric value rather than the name. That keeps the schema compact and avoids string comparison. However, if you later change the numeric values, existing rows become invalid. Custom values let you define a stable mapping that survives code changes.

For JSON serialization, the default behavior of System.Text.Json writes the numeric value. If you need the name instead, you can use the JsonStringEnumConverter. That converter works with custom values because it maps the name to the numeric value internally. The converter does not care what the numeric values are; it only needs the member names.

var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter() } }; string json = JsonSerializer.Serialize(HttpStatusCode.Ok, options); // Output: Ok

When you deserialize, the converter matches the string name back to the enum member. Since C# already requires member names to be unique, string-based serialization is unaffected by duplicate numeric values. For production systems, avoid duplicate values unless you have a specific reason.

Performance and Maintainability Considerations

Enum operations are generally fast. If you are parsing a large or repeated set of codes in a hot path, cache the converted values rather than calling Enum.Parse every time.

From a maintainability perspective, custom values make the code more explicit but also add a maintenance burden. Every time you add a new member, you must choose a value that does not conflict with existing ones unless aliases are intentional. Use a naming convention or a comment to explain the value's origin when it comes from an external spec.

Another concern is compatibility across versions. If you ship a library that exposes an enum with custom values, changing those values is a breaking change for consumers who have serialized data. Even adding a new member with a value that was previously unused is safe, but reusing a value that was previously assigned to a different member is not. Treat the numeric values as part of your public API.

Finally, be careful when using Enum.GetValues and Enum.GetNames with custom values. GetValues returns underlying values, so duplicate numeric aliases can be ambiguous or require a Distinct() call. GetNames returns every member name, even when values are shared. Choose the method that matches your intent: numeric codes or possible names.

string[] names = Enum.GetNames<Result>(); // Contains Success, Ok, and Failure; order is not guaranteed.

In practice, custom enum values are a simple feature with deep implications for data integrity and API stability. Define them deliberately, document the source of each value, and test the parsing and serialization paths that depend on them.

C# Enum Custom Values: Practical Usage and Code Examples | RYUSLOG DEV