Updated on
A C# enum serializes as a number by default. To get its name instead, System.Text.Json takes a JsonStringEnumConverter and Newtonsoft.Json takes a StringEnumConverter, applied to one property, one enum type, one serializer call, or the whole application.
DataContractJsonSerializer is the exception: it has no converter and no way to do this at all, it writes every enum as a number and ignores [EnumMember]. This article covers all three, from a single decorated property up to a global setting, plus flags enums and custom strings.
Let’s take a look.
Why Does C# Serialize an Enum as a Number by Default?
Both System.Text.Json and Newtonsoft.Json write an enum as its underlying integer unless we tell them otherwise. Color.LightGray becomes 1, not "LightGray".
The integer is the enum’s real value. The member name lives in metadata, and reaching it costs something: the .NET AOT analyzer raises IL3050 on JsonStringEnumConverter precisely because reading those names requires runtime code generation.
That default breaks interoperability. A JavaScript client receiving "backColor": 1 has to keep its own copy of our enum ordering, and any reorder of the members silently changes the meaning of every document already stored.
Inserting a member in the middle is the failure everyone hits. Add one value to Color and every persisted 1 now means something else, with nothing in the payload to say so.
Serializing the name instead makes the payload self-describing. "backColor": "LightGray" survives a reorder, reads correctly in a log, and needs no shared constant table on the consuming side.
If the value never goes into JSON at all, we do not need a serializer for this. There are simpler ways to get an enum member as a string without a serializer.
To begin, let’s prepare a few object models:
public class Canvas
{
public static Canvas Poster
=> new() { Name = "Poster", BackColor = Color.LightGray, Pen = new ("Simple", Color.Red) };
public string? Name { get; set; }
public Color BackColor { get; set; }
public Medium Medium { get; set; }
public Pen? Pen { get; set; }
}
public record struct Pen(string Name, Color Color);
public enum Color
{
White, LightGray, DarkGray, Red
}
public enum Medium
{
Water, Oil
}
We declare two enums: Color and Medium, a record Pen, and a class Canvas. Canvas is our primary model in concern. Also, we declare a static Canvas instance (Poster) for convenient use in examples.
Next, we are going to add a basic Serialize method in the base class (UnitTestBase):
// Native
public static string Serialize(object obj)
{
return JsonSerializer.Serialize(obj);
}
// Newtonsoft
public static string Serialize(object obj)
{
return JsonConvert.SerializeObject(obj);
}
All set, we’re ready to go.
First, let’s check the default behavior of serialization on Canvas.Poster object:
var json = Serialize(Canvas.Poster);
And let’s inspect the result:
{
"Name": "Poster",
"BackColor": 1,
"Medium": 0,
"Pen": {
"Name": "Simple",
"Color": 3
}
}
No wonder, the resulting string contains the enum properties (BackColor, Medium, Pen.Color) as integer values.
So, the question arises: “Can enum be serialized to a string in C#”? Let’s look for the answer in the rest of the article.
Which Converter Serializes an Enum as a String?
Three converters cover almost every case, and choosing one is a question of which library does the writing.
System.Text.Json uses JsonStringEnumConverter from System.Text.Json.Serialization. We register it on a property, on the enum type, on a JsonSerializerOptions instance, or once for the whole application.
Newtonsoft.Json uses StringEnumConverter from Newtonsoft.Json.Converters. It takes the same four placements and accepts a naming strategy.
DataContractJsonSerializer has no converter to add at all. It writes every enum as a number and ignores [EnumMember], so a string is simply not on offer there.
Both attribute placements are selective. On a property, only that property changes; on the enum declaration, every property of that type changes everywhere it appears.
Scope matters more than library. A converter registered globally is invisible in the model, which is usually what we want, while a property-level [JsonConverter] overrides a globally registered one, so the attribute is the narrower and the stronger of the two.
| Library | Type to use | Custom name attribute |
|---|---|---|
System.Text.Json | JsonStringEnumConverter | JsonStringEnumMemberName |
System.Text.Json (source-generated / AOT) | JsonStringEnumConverter<TEnum> | JsonStringEnumMemberName |
Newtonsoft.Json | StringEnumConverter | EnumMember |
DataContractJsonSerializer | None, it cannot | None, always a number |
Serialization of an Enum Property
First of all, we want to serialize the BackColor property as a string. So, it’s time to change the Canvas model and decorate the BackColor property with the converter attribute:
// Native
public class Canvas
{
...
[JsonConverter(typeof(JsonStringEnumConverter))]
public Color BackColor { get; set; }
...
}
// Newtonsoft
public class Canvas
{
...
[JsonConverter(typeof(StringEnumConverter))]
public Color BackColor { get; set; }
...
}
Now the serialization of Canvas.Poster :
var json = Serialize(Canvas.Poster);
Produces a different output:
{
"Name": "Poster",
"BackColor": "LightGray",
"Medium": 0,
"Pen": {
"Name": "Simple",
"Color": 3
}
}
We can see that BackColor turns to string but Medium and Pen.Color don’t, because they don’t have the converter attribute applied. This means we can selectively serialize specific enum properties in this way.
Serialization of an Enum Type
Next, we want to serialize all instances of the Color enum to strings. We can do this by applying the converter attribute on the enumeration type itself instead of the properties:
public class Canvas
{
...
public Color BackColor { get; set; }
...
}
// Native
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Color
{
White, LightGray, DarkGray, Red
}
// Newtonsoft
[JsonConverter(typeof(StringEnumConverter))]
public enum Color
{
White, LightGray, DarkGray, Red
}
Again, after the serialization, the end result differs from the previous one:
{
"Name": "Poster",
"BackColor": "LightGray",
"Medium": 0,
"Pen": {
"Name": "Simple",
"Color": "Red"
}
}
This time we can see all Color instances (BackColor and Pen.Color) turn to string, but Medium enum still follows the default behavior. So in this way, we can selectively serialize some specific enum types to string.
How Do We Serialize One Object’s Enums as Strings?
We can’t go with the attribute-based approaches in some cases such as:
- Dealing with system types or third party library models
- Serialize objects generated on the fly e.g. anonymous objects
- Don’t want to pollute our domain models but still want to get enum as a string
- Stringify enums only for a particular object instance, not all instances in general
In such cases, if we don’t bother about selective serialization, we can instruct the serializer to convert enum on-demand. Both libraries offer an overload to pass converters in line with the serialization method. So, let’s implement our second serialization routine in the base class:
// Native
public static string SerializeWithStringEnum(object obj)
{
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
return JsonSerializer.Serialize(obj, options);
}
// Newtonsoft
public static string SerializeWithStringEnum(object obj)
{
var converter = new StringEnumConverter();
return JsonConvert.SerializeObject(obj, converter);
}
In the case of the native version, we instantiate a JsonSerializerOptions class. Then we register the enum converter there and finally call the appropriate Serialize method.
Things are a bit straightforward for Newtonsoft. We can directly pass the converter to the serializing method.
Next, we are going to get rid of the converter attributes from all our models and instead apply this new method on Canvas.Poster and an anonymous object:
var poster = SerializeWithStringEnum(Canvas.Poster);
var schedule = SerializeWithStringEnum(new { Description = "Exhibition", Day = DayOfWeek.Monday });
Now, we can inspect the result:
/* poster */
{
"Name": "Poster",
"BackColor": "LightGray",
"Medium": "Water",
"Pen": {
"Name": "Simple",
"Color": "Red"
}
}
/* schedule */
{
"Description": "Exhibition",
"Day": "Monday"
}
As we expect, all enums of the target objects are serialized in a string form.
How Do We Make Every Enum Serialize as a String?
Attribute-based ways give us the flexibility to manipulate the enum serialization in a controlled way. And the options-based way allows us to deal with specific object instances on demand. But, is there any way to make all enums serialized as strings by default? Yes, there is!
To make the string-enum converter default choice for enum serialization, we need some bootstrapping that varies depending on the application types. Typically this means, we have to register the enum converter in the DI pipeline. Here, we will focus on ASP.NET Core applications mainly. The enum converter is one of several things we can set these options globally for the whole application.
Configure ASP.NET Core Web API
We are going to start with a basic ASP.NET Core Web API project and remove all auto-generated controllers and models. Next, let’s add our object models as usual and a CanvasController:
[ApiController]
[Route("[controller]")]
public class CanvasController : ControllerBase
{
[HttpGet("poster")]
public Canvas GetPoster() => Canvas.Poster;
[HttpGet("schedule")]
public object GetSchedule() => new { Description = "Exhibition", Day = DayOfWeek.Monday };
}
This is a typical Web API controller with two simple endpoints: “canvas/poster” and “canvas/schedule”. They provide output for Canvas.Poster and an anonymous object respectively.
Now, let’s move to the entry point, the Program class:
// Native
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
var app = builder.Build();
app.MapControllers();
app.Run();
The highlighted part is all that we need to change in this boilerplate code. We just get into the DI pipeline using the AddJsonOptions method and get access to the JsonSerializerOptions configuration. This is where we can register the native string-enum converter. That’s all! Every time the framework attempts to serialize enum to JSON, it will pick this converter from the DI pipeline.
We can do this for Newtonsoft in a similar fashion:
...
builder.Services.AddControllers()
.AddNewtonsoftJson(options =>
{
options.SerializerSettings.Converters.Add(new StringEnumConverter());
});
...
Again, all we need is to find the appropriate DI configurator method to use the Newtonsoft serializer. For this, we have to install Microsoft.AspNetCore.Mvc.NewtonsoftJson package. This provides the AddNewtonsoftJson configurator method. It exposes the serializer settings where we can register the Newtonsoft enum converter.
Now, we can examine the output of our API endpoints from Postman or directly from a browser:
/* http://localhost:5045/canvas/poster */
{
"name": "Poster",
"backColor": "LightGray",
"medium": "Water",
"pen": {
"name": "Simple",
"color": "Red"
}
}
/* http://localhost:5045/canvas/schedule */
{
"description": "Exhibition",
"day": "Monday"
}
We get exactly what we desire, all string-enums by default!
Configure ASP.NET Core Minimal API
Next, we are going to explore the configuration for Minimal API. This is Web API at its core, but the bootstrapping steps are a bit different. As of .NET 10, Minimal APIs have no built-in Newtonsoft.Json integration: AddNewtonsoftJson() configures MVC, not the Minimal API pipeline. So, we are going to discuss only the System.Text.Json library.
Let’s modify our auto-generated Program class and add the bootstrapping code:
var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
var app = builder.Build();
app.MapGet("/poster", () => Canvas.Poster);
app.MapGet("/schedule", () => new { Description = "Exhibition", Day = DayOfWeek.Monday });
app.Run();
This time, we call ConfigureHttpJsonOptions to register the string-enum converter. It hands us the same Microsoft.AspNetCore.Http.Json.JsonOptions object that a raw Configure<JsonOptions> call reaches, only as a first-class extension. In addition, we map two routes: poster and schedule. These endpoints work the same as our previous controller examples and the output is also identical.
As we can see, we can set up our application for serializing enums as strings by default without any extra model decoration! In this way, we can work with clean models. Also, we don’t need to worry about specifying explicit options every time we serialize an object.
Configure Source-Generated Serialization
Source generation removes the reflection that JsonStringEnumConverter normally does, which matters when the application is trimmed or published ahead-of-time. In an AOT-analyzed project the non-generic converter raises IL3050, and the diagnostic itself says applications should use the generic JsonStringEnumConverter<TEnum> instead. A source-generated context is the cleanest way to get there:
[JsonSourceGenerationOptions(UseStringEnumConverter = true)] [JsonSerializable(typeof(Canvas))] internal partial class CanvasContext : JsonSerializerContext;
Every enum reachable from Canvas now serializes by name, with no converter registered at runtime and no reflection at all:
var json = JsonSerializer.Serialize(Canvas.Poster, CanvasContext.Default.Canvas);
The UseStringEnumConverter property arrived in .NET 9, so this shape needs .NET 9 or later.
| I want it to apply to... | System.Text.Json | Newtonsoft.Json |
|---|---|---|
| one property | [JsonConverter(typeof(JsonStringEnumConverter))] on the property | [JsonConverter(typeof(StringEnumConverter))] on the property |
| every use of one enum type | the same attribute on the enum declaration | the same attribute on the enum declaration |
| one serializer call | options.Converters.Add(new JsonStringEnumConverter()) | pass new StringEnumConverter() to SerializeObject |
| a whole MVC / Web API app | AddControllers().AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(...)) | AddControllers().AddNewtonsoftJson(o => o.SerializerSettings.Converters.Add(...)) |
| a whole Minimal API app | builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Converters.Add(...)) | No built-in support as of .NET 10 |
How Do We Serialize an Enum to a Custom String Value?
While serializing enum to a string, we may want some fine-tuning. For example, transform to camelCase or expose as a more meaningful text, etc. Let’s explore how we can do these.
Serialize to a camelCase Text
JSON serialization to a camelCase string is a common practice. This is in fact the default behavior for ASP.NET Core. But in general, this means camelCase transformation of property names, not their values, and there is a separate article on how to camelCase the property names as well:
// Native
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
options.Converters.Add(new JsonStringEnumConverter());
var json = JsonSerializer.Serialize(Canvas.Poster, options);
{
"name": "Poster",
"backColor": "LightGray",
"medium": "Water",
"pen": {
"name": "Simple",
"color": "Red"
}
}
This is self-explanatory and technically logical but may not be desirable for enums in practice. That’s why the enum converter offers a way to explicitly transform enum values to camelCase:
// Native var options = new JsonSerializerOptions(); options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)); var json = JsonSerializer.Serialize(Canvas.Poster, options); // Newtonsoft var converter = new StringEnumConverter(new CamelCaseNamingStrategy()); var json = JsonConvert.SerializeObject(Canvas.Poster, converter);
We just need to specify the camelCase policy (or strategy) during the enum converter instantiation and pass it during serialization as usual:
{
"Name": "Poster",
"BackColor": "lightGray",
"Medium": "water",
"Pen": {
"Name": "Simple",
"Color": "red"
}
}
All the enum values are now in camelCase format. The converter takes any naming policy, not just the built-in ones, so we can also write a custom JsonNamingPolicy for the naming strategy and pass that instead.
Serialize as Custom Text
Because of language’s variable naming rules, an enum member can’t always convey meaningful text when serialized in a regular way. To get around this problem, we generally use EnumMemberAttribute from System.Runtime.Serialization assembly that is specifically introduced for this purpose:
public record struct ToggleControl(string Name, ToggleType Type);
public enum ToggleType
{
[EnumMember(Value = "Enable/Disable")]
EnableDisable,
[EnumMember(Value = "Visible/Hidden")]
VisibleHidden,
[EnumMember(Value = "Editable/Readonly")]
EditableReadonly,
}
We define a ToggleType enum that has EnumMemberAttribute on its members to provide more meaningful values than their names. We also define a ToggleControl record that uses this enum.
Newtonsoft honors that attribute directly:
// Newtonsoft
var controls = new ToggleControl[]
{
new("toggle1", ToggleType.EnableDisable),
new("toggle2", ToggleType.VisibleHidden)
};
var json = SerializeWithStringEnum(controls);
[
{
"Name": "toggle1",
"Type": "Enable/Disable"
},
{
"Name": "toggle2",
"Type": "Visible/Hidden"
}
]
We create an array of ToggleControl with different ToggleType values. On serialization, we can see it produces output with our desired texts!
System.Text.Json ignores [EnumMember], and it always has. It has its own attribute for the same job, JsonStringEnumMemberNameAttribute, added in .NET 9:
public enum ToggleType
{
[JsonStringEnumMemberName("Enable/Disable")]
EnableDisable,
[JsonStringEnumMemberName("Visible/Hidden")]
VisibleHidden,
}
With JsonStringEnumConverter registered, those are the strings that come out, and deserialization reads them back to the right member. Without a registered converter the attribute does nothing at all and the same model still writes {"Name":"toggle1","Type":0}, so the converter is not optional here.
Before this attribute existed, the only option was a hand-written converter, and that is still worth knowing for the cases the attribute does not cover: here is how to write a custom JsonConverter.
How Do We Serialize an Enum With DataContractJsonSerializer?
DataContractJsonSerializer lives in System.Runtime.Serialization.Json and predates both libraries above. It is the serializer WCF services and ASP.NET AJAX endpoints were built on, and it is still in the box.
It cannot serialize an enum as a string. There is no converter to register and no setting to change: every enum member is written as its underlying number.
[EnumMember] does not change this. Decorate a member with [EnumMember(Value = "Enable/Disable")] and the output is still 0. Microsoft’s documentation says the attribute is simply ignored here.
That is the surprise, because the XML DataContractSerializer does honour it. The same enum, the same attribute, written to XML gives the name and written to JSON gives the number.
Deserialization matches. A payload containing the string throws rather than parsing, while any number is accepted, even one no member defines.
So if the output has to read as a name, this is the wrong serializer. JsonStringEnumConverter and StringEnumConverter both do it; this one has no equivalent.
Here is the whole thing in one sample. The class needs no package reference on .NET 10, it resolves from the shared framework:
[DataContract]
public class ToggleSet
{
[DataMember]
public ToggleState Decorated { get; set; }
[DataMember]
public ToggleState Undecorated { get; set; }
}
[DataContract]
public enum ToggleState
{
[EnumMember(Value = "Enable/Disable")]
EnableDisable = 0,
[EnumMember(Value = "Visible/Hidden")]
VisibleHidden = 1,
}
var set = new ToggleSet
{
Decorated = ToggleState.EnableDisable,
Undecorated = ToggleState.VisibleHidden
};
var serializer = new DataContractJsonSerializer(typeof(ToggleSet));
using var stream = new MemoryStream();
serializer.WriteObject(stream, set);
var json = Encoding.UTF8.GetString(stream.ToArray());
Both members carry [EnumMember], and both come out as numbers:
{"Decorated":0,"Undecorated":1}
Feeding the attribute’s own string back in is a hard failure rather than a fallback. Reading {"Decorated":"Enable/Disable","Undecorated":1} throws a SerializationException saying the value cannot be parsed as the type Int64. A number the enum never declares, on the other hand, deserializes without complaint.
Swap in the XML DataContractSerializer and the same type writes <Decorated>Enable/Disable</Decorated>. The attribute is not being ignored by the data contract model, only by its JSON writer.
JSON Serialization of Flag Enums
Default serialization of flag enums (bit-mask enum) produces even more odd output. Instead of representing as a combination of flags, they’re exposed as a combined value. For a refresher on how the [Flags] attribute works, we have a dedicated article:
[Flags]
public enum TextStyles
{
None = 0,
Bold = 1,
Italic = 2,
Underline = 4,
}
var styles = TextStyles.Bold | TextStyles.Italic | TextStyles.Underline;
var json = Serialize(new { Format = styles });
The output:
{"Format":7}
We start with defining a TextStyles flag. Then we declare a styles which is a combination of Bold(1), Italic(2), Underline(4) flags. For default serialization, we may expect it to serialize as “1, 2, 4” at least. But, we get a 7 instead because the default serializer just cares about the numeric value for enum.
The string-enum conversion is free from this oddness:
var styles = TextStyles.Bold | TextStyles.Italic | TextStyles.Underline;
var json = SerializeWithStringEnum(new { Format = styles });
Now we have a different result:
{"Format":"Bold, Italic, Underline"}
That’s the output we desire! Deserialization reads that comma-separated form back into the same combination, so a flags enum round-trips as a string.
Conclusion
In this article, we have learned a few ways of JSON serialization of enum as a string. We have also discussed various techniques to get customized string serialization of an enum.
The short version: register JsonStringEnumConverter for System.Text.Json or StringEnumConverter for Newtonsoft.Json, and pick the placement that matches the scope we want. DataContractJsonSerializer is the one serializer that cannot do it at all. Going the other way, from text back to a member, is a different job: see how to convert a string or an int back into an enum.
Tested with .NET 10 and Newtonsoft.Json 13.0.4.
