Data and Formats · Lesson 17

String enums with an explicit numeric policy

Make wire values readable while deciding whether unknown numeric values are accepted.

How it works

An enum can be represented in JSON as a number or as a string name. A string converter makes payloads easier to inspect, but it also creates a public naming contract. The sample configures a generic JsonStringEnumConverter for State and explicitly disallows integer values.

Serializing Ready produces a JSON string, including the quotes that belong to JSON syntax. Reading the JSON string Done produces the corresponding enum value. Reading the numeric token one is rejected because this configuration intentionally accepts named states rather than numeric representations.

Without an explicit policy, numeric enum values can be especially surprising because an enum's underlying integer type can represent numbers beyond the declared names. A successful numeric conversion does not necessarily prove that the application recognizes the resulting state. Validate allowed states and choose converter behavior deliberately.

Changing a C# enum member's name can change the wire representation expected by other components. Refactoring an internal symbol is therefore not always an internal-only change. If a schema needs long-term stable external names, define and test that mapping at the serialization boundary rather than depending on accidental source-code spelling.

Practice by adding a Failed state and round-tripping each declared value. Try an unknown string and a numeric token, observing which inputs the chosen converter rejects. Do not catch every failure and substitute Ready, because that would turn unrecognized input into a misleading valid state.

Finally, decide how a future state should affect an older client. A strict client may reject the payload, while a compatibility design may preserve an unknown textual value in a different model. The enum representation, accepted values, and versioning behavior should be designed together so readable JSON does not conceal an unstable or overly permissive contract.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;

var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter<State>(allowIntegerValues: false));
Console.WriteLine(JsonSerializer.Serialize(State.Ready, options));
Console.WriteLine(JsonSerializer.Deserialize<State>("\"Done\"", options));
try { JsonSerializer.Deserialize<State>("1", options); }
catch (JsonException) { Console.WriteLine("integer rejected"); }
enum State { Ready, Done }

Expected output

"Ready"
Done
integer rejected
Try this
The integer setting on the converter is part of the wire-format contract.