Back to Blog
C#

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

c# enum custom values: Learn how to define enums with custom numeric values in C#, parse them, handle duplicates, and use them in 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 like a database column, a wire protocol, or a legacy API, the numbers must match the contract exactly. This article explains how to assign c# enum custom values, 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 given a constant expression of the underlying type. The underlying type defaults to int, but you can change it to byte, short, long, or any other integral type.

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 would get 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

The Enum.Parse and Enum.TryParse methods work with both the member name and the numeric value. When you pass a string that contains a number, the runtime first tries to match it against the underlying values. If a match is found, it returns the corresponding enum member.

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

Be aware that Enum.Parse is case-sensitive by default. If you need case-insensitive matching, pass true for the ignoreCase parameter. Also, 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 or use Convert.ToInt32.

HttpStatusCode status = HttpStatusCode.NotFound; Console.WriteLine(status.ToString()); // NotFound Console.WriteLine((int)status); // 404 Console.WriteLine(Convert.ToInt32(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 one of them, but the choice is not guaranteed to be deterministic across .NET versions. In practice, it returns the first member defined in the source code, but you should not rely on that behavior. If you need a canonical member, avoid aliases or handle the mapping explicitly.

Duplicate values also affect Enum.GetValues. The method returns all distinct values, not all distinct names. If you iterate over the enum to build a dropdown or a list of codes, you will see each numeric value only once. That can be surprising if you expected one entry per member.

foreach (var value in Enum.GetValues<Result>()) { Console.WriteLine($"{(int)value} {value}"); } // Output: // 1 Success // 2 Failure

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. When you use BinaryFormatter or System.Text.Json with the default converter, the numeric value is written as the underlying type. If you change the underlying type from int to long, the serialized output changes. 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. If you have duplicate names, the behavior is undefined. For production systems, avoid duplicate names and duplicate values unless you have a specific reason.

Performance and Maintainability Considerations

Enum operations are generally fast. Parsing a string to an enum uses a dictionary lookup internally, which is O(1) for the common case. The first parse for a given enum type may incur some initialization cost, but subsequent calls are cheap. If you are parsing many values in a tight loop, consider caching the results if the input set is limited.

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 with custom values. The method returns the distinct numeric values, not the distinct names. If you need to iterate over all members, including aliases, you must use Enum.GetNames instead. This distinction becomes important when generating documentation or building UI filters.

string[] names = Enum.GetNames<Result>(); // Returns "Success", "Ok", "Failure"

Using Enum.GetNames gives you every member, even those that share a value. This is the correct approach when you want to display all possible names to a user. The choice between GetValues and GetNames depends on whether you care about the underlying numeric identity or the named members.

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