Por padrão, uma enumeração em C# é serializada como número. Para obter o nome em vez do número, o System.Text.Json usa um JsonStringEnumConverter e o Newtonsoft.Json usa um StringEnumConverter, aplicados a uma propriedade, a um tipo de enumeração, a uma chamada do serializador ou à aplicação inteira.

O DataContractJsonSerializer é a exceção: ele não tem conversor nem qualquer outra forma de fazer isso, escreve toda enumeração como número e ignora [EnumMember]. Este artigo aborda os três, de uma única propriedade decorada até uma configuração global, além de enumerações de flags e strings personalizadas.

Para baixar o código-fonte deste artigo, você pode acessar nosso repositório no GitHub.

Vamos dar uma olhada.

Por que o C# serializa uma enumeração como número por padrão?

Tanto o System.Text.Json quanto o Newtonsoft.Json escrevem uma enumeração como o inteiro subjacente, a menos que digamos o contrário. Color.LightGray vira 1, e não "LightGray".

O inteiro é o valor real da enumeração. O nome do membro fica nos metadados, e chegar até ele tem um custo: o analisador de AOT do .NET emite IL3050 em JsonStringEnumConverter justamente porque criar os conversores dele exige geração de código em tempo de execução.

Esse comportamento padrão quebra a interoperabilidade. Um cliente JavaScript que recebe "backColor": 1 precisa manter a própria cópia da ordem da nossa enumeração, e qualquer reordenação dos membros muda silenciosamente o significado dos documentos já armazenados.

Inserir um membro no meio é a falha em que todo mundo esbarra. Adicione um valor antes de LightGray e todo 1 persistido passa a significar outra coisa, sem nada no payload que avise disso.

Serializar o nome deixa o payload autodescritivo. "backColor": "LightGray" sobrevive a uma reordenação, é lido corretamente em um log e não exige uma tabela de constantes compartilhada do lado de quem consome os dados.

Se o valor nunca vai para JSON, não precisamos de um serializador para isso. Há formas mais simples de obter um membro de enumeração como string sem serializador.

Para começar, vamos preparar alguns modelos de objeto:

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
}

Declaramos duas enumerações (Color e Medium), um record Pen e uma classe Canvas. Canvas é o nosso modelo principal. Também declaramos uma propriedade estática do tipo Canvas (Poster) para facilitar o uso nos exemplos.

Em seguida, vamos adicionar um método Serialize básico à classe base (UnitTestBase), com as linhas global using no início do arquivo da classe. Usamos um projeto de console para cada biblioteca, e, no projeto do Newtonsoft, é preciso rodar dotnet add package Newtonsoft.Json. Fora das seções de ASP.NET Core, os tipos dos trechos de código ficam no nível do arquivo nesses projetos, e as instruções de cada trecho vão em um método próprio, dentro de uma classe que deriva de UnitTestBase:

// Native
global using System.Text.Json;
global using System.Text.Json.Serialization;

public static string Serialize(object obj)
{
    return JsonSerializer.Serialize(obj);
}

// Newtonsoft
global using Newtonsoft.Json;
global using Newtonsoft.Json.Converters;
global using Newtonsoft.Json.Serialization;

public static string Serialize(object obj)
{
    return JsonConvert.SerializeObject(obj);
}

Tudo pronto, podemos começar.

Primeiro, vamos verificar o comportamento padrão da serialização com o objeto Canvas.Poster:

var json = Serialize(Canvas.Poster);

E vamos examinar o resultado:

{
  "Name": "Poster",
  "BackColor": 1,
  "Medium": 0,
  "Pen": {
    "Name": "Simple",
    "Color": 3
  }
}

Sem surpresa, a string resultante contém as propriedades de enumeração (BackColor, Medium, Pen.Color) como valores inteiros.

Então, surge a pergunta: “É possível serializar uma enumeração como string em C#?” Vamos procurar a resposta no restante do artigo.

Qual conversor serializa uma enumeração como string?

Três conversores cobrem quase todos os casos, e a escolha depende de qual biblioteca faz a escrita.

O System.Text.Json usa o JsonStringEnumConverter, de System.Text.Json.Serialization. Podemos registrá-lo em uma propriedade, no tipo de enumeração, em uma instância de JsonSerializerOptions ou uma única vez para a aplicação inteira.

O Newtonsoft.Json usa o StringEnumConverter, de Newtonsoft.Json.Converters. Ele admite os mesmos quatro posicionamentos e aceita uma estratégia de nomenclatura.

O DataContractJsonSerializer não tem nenhum conversor para adicionar. Ele escreve toda enumeração como número e ignora [EnumMember], então string simplesmente não é uma opção ali.

Os dois posicionamentos do atributo são seletivos. Em uma propriedade, só essa propriedade muda; na declaração da enumeração, toda propriedade desse tipo muda em qualquer lugar em que apareça.

O escopo importa mais do que a biblioteca. Um conversor registrado globalmente não aparece no modelo, o que geralmente é o que queremos, enquanto um [JsonConverter] no nível da propriedade prevalece sobre um registrado globalmente; assim, o atributo é o mais restrito e o mais forte dos dois.

BibliotecaTipo a usarAtributo de nome personalizado
System.Text.JsonJsonStringEnumConverterJsonStringEnumMemberName
System.Text.Json (geração de código-fonte / AOT)JsonStringEnumConverter<TEnum>JsonStringEnumMemberName
Newtonsoft.JsonStringEnumConverterEnumMember
DataContractJsonSerializerNenhum, não é possívelNenhum, sempre um número

Serialização de uma propriedade de enumeração

Antes de tudo, queremos serializar a propriedade BackColor como string. Então, é hora de alterar o modelo Canvas e decorar a propriedade BackColor com o atributo do conversor:

// Native
public class Canvas
{
    ...
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public Color BackColor { get; set; }
    ...
}

// Newtonsoft
public class Canvas
{
    ...
    [JsonConverter(typeof(StringEnumConverter))]
    public Color BackColor { get; set; }
    ...
}

Agora, a serialização de Canvas.Poster:

var json = Serialize(Canvas.Poster);

Produz uma saída diferente:

{
  "Name": "Poster",
  "BackColor": "LightGray",
  "Medium": 0,
  "Pen": {
    "Name": "Simple",
    "Color": 3
  }
}

Vemos que BackColor passa a ser string, mas Medium e Pen.Color não, porque não têm o atributo do conversor aplicado. Isso significa que, dessa forma, podemos serializar seletivamente propriedades enum específicas.

Serialização de um tipo de enumeração

Agora, queremos serializar como string todas as instâncias da enumeração Color. Podemos fazer isso aplicando o atributo do conversor ao próprio tipo de enumeração, em vez das propriedades:

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
}

Mais uma vez, após a serialização, o resultado final é diferente do anterior:

{
  "Name": "Poster",
  "BackColor": "LightGray",
  "Medium": 0,
  "Pen": {
    "Name": "Simple",
    "Color": "Red"
  }
}

Desta vez, vemos que todas as instâncias de Color (BackColor e Pen.Color) viram string, mas a enumeração Medium continua seguindo o comportamento padrão. Assim, podemos serializar seletivamente como string alguns tipos de enumeração específicos.

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

Como serializar como string as enumerações de um único objeto?

Em alguns casos, não dá para usar as abordagens baseadas em atributos, como:

  • Quando lidamos com tipos do sistema ou modelos de bibliotecas de terceiros
  • Quando serializamos objetos criados dinamicamente, p. ex. objetos anônimos
  • Quando não queremos poluir os modelos de domínio, mas ainda queremos a enumeração como string
  • Quando queremos converter enumerações em string apenas para uma instância específica de objeto, e não para todas as instâncias em geral

Nesses casos, se a serialização seletiva não nos preocupa, podemos instruir o serializador a converter as enumerações sob demanda. As duas bibliotecas oferecem uma sobrecarga do método de serialização que permite passar conversores na própria chamada. Então, vamos implementar a segunda rotina de serialização na classe base:

// 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);
}

Na versão nativa, criamos uma instância da classe JsonSerializerOptions. Em seguida, registramos nela o conversor de enumeração e, por fim, chamamos o método Serialize adequado.

Com o Newtonsoft, as coisas são um pouco mais simples. Podemos passar o conversor diretamente para o método de serialização.

Em seguida, vamos remover os atributos de conversor de todos os modelos e, em vez disso, aplicar esse novo método a Canvas.Poster e a um objeto anônimo:

var poster = SerializeWithStringEnum(Canvas.Poster);
var schedule = SerializeWithStringEnum(new { Description = "Exhibition", Day = DayOfWeek.Monday });

Agora, podemos examinar o resultado:

/* poster */
{
  "Name": "Poster",
  "BackColor": "LightGray",
  "Medium": "Water",
  "Pen": {
    "Name": "Simple",
    "Color": "Red"
  }
}
/* schedule */
{
  "Description": "Exhibition",
  "Day": "Monday"
}

Como esperado, todas as enumerações dos objetos-alvo são serializadas como string.

Como serializar todas as enumerações como string?

As abordagens baseadas em atributos nos dão flexibilidade para manipular a serialização de enumerações de forma controlada. Já a abordagem baseada em opções nos permite tratar instâncias específicas de objetos sob demanda. Mas existe alguma forma de fazer todas as enumerações serem serializadas como string por padrão? Sim, existe!

Para tornar o conversor de enumeração para string a escolha padrão na serialização de enumerações, precisamos de uma configuração inicial que varia conforme o tipo de aplicação. Normalmente, isso significa registrar o conversor de enumeração no contêiner de injeção de dependência (DI). Aqui, vamos nos concentrar principalmente em aplicações ASP.NET Core. O conversor de enumeração é uma das várias coisas que podemos configurar ao definir essas opções globalmente para a aplicação inteira.

Configurar uma Web API do ASP.NET Core

Vamos começar com um projeto básico de ASP.NET Core Web API e remover todos os controladores e modelos gerados automaticamente. Em seguida, vamos adicionar os nossos modelos de objeto, como de costume, e um CanvasController:

using Microsoft.AspNetCore.Mvc;

[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 };
}

É um controlador típico de Web API, com dois endpoints simples: “canvas/poster” e “canvas/schedule”. Eles retornam, respectivamente, Canvas.Poster e um objeto anônimo.

Agora, vamos para o ponto de entrada, a classe Program:

// Native
using System.Text.Json.Serialization;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
    });

var app = builder.Build();

app.MapControllers();

app.Run();

A parte destacada é tudo o que precisamos alterar nesse código repetitivo (boilerplate). Basta chamar o método AddJsonOptions ao registrar os serviços no contêiner de DI e acessar a configuração de JsonSerializerOptions. É aí que podemos registrar o conversor nativo de enumeração para string. Pronto! Sempre que o MVC tentar serializar uma enumeração em JSON, vai usar esse conversor registrado no contêiner de DI.

Com o Newtonsoft, podemos fazer o mesmo de forma parecida:

using Newtonsoft.Json.Converters;

...
builder.Services.AddControllers()
    .AddNewtonsoftJson(options =>
    {
        options.SerializerSettings.Converters.Add(new StringEnumConverter());
    });
...

Mais uma vez, basta encontrar o método de configuração de DI adequado para usar o serializador do Newtonsoft. Para isso, precisamos instalar o pacote Microsoft.AspNetCore.Mvc.NewtonsoftJson, que fornece o método de configuração AddNewtonsoftJson. Ele expõe as configurações do serializador, nas quais podemos registrar o conversor de enumeração do Newtonsoft.

Agora, podemos examinar a saída dos endpoints da API no Postman ou diretamente no navegador:

/* 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"
}

Obtemos exatamente o que queríamos: todas as enumerações como string, por padrão!

Configurar uma API mínima do ASP.NET Core

Agora, vamos ver a configuração para APIs mínimas. No fundo, é uma Web API, mas os passos de configuração inicial são um pouco diferentes. Até o .NET 10, as APIs mínimas não têm integração nativa com o Newtonsoft.Json: AddNewtonsoftJson() configura o MVC, e não o pipeline das APIs mínimas. Por isso, vamos tratar apenas da biblioteca System.Text.Json.

Em um novo projeto de API mínima com os nossos modelos de objeto, vamos modificar a classe Program gerada automaticamente e adicionar o código de configuração inicial:

using System.Text.Json.Serialization;

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();

Desta vez, chamamos ConfigureHttpJsonOptions para registrar o conversor de enumeração para string. Ele nos dá o mesmo objeto Microsoft.AspNetCore.Http.Json.JsonOptions que uma chamada direta a Configure<JsonOptions> alcançaria, só que como um método de extensão próprio. Além disso, mapeamos duas rotas: poster e schedule. Esses endpoints funcionam da mesma forma que os exemplos anteriores com controlador, e a saída também é idêntica.

Como vemos, é possível configurar a aplicação para serializar enumerações como string por padrão sem nenhuma decoração extra nos modelos! Assim, trabalhamos com modelos limpos. Além disso, não precisamos nos preocupar em especificar opções explícitas toda vez que serializamos um objeto.

Configurar a serialização com geração de código-fonte

A geração de código-fonte elimina a geração de código em tempo de execução de que o JsonStringEnumConverter normalmente precisa, o que importa quando a aplicação é publicada com compilação AOT (ahead-of-time). Em um projeto analisado para AOT, o conversor não genérico emite IL3050, e o próprio diagnóstico diz que as aplicações devem usar o JsonStringEnumConverter<TEnum> genérico em vez dele. Um contexto de geração de código-fonte é a forma mais limpa de chegar lá:

[JsonSourceGenerationOptions(UseStringEnumConverter = true)]
[JsonSerializable(typeof(Canvas))]
internal partial class CanvasContext : JsonSerializerContext;

Agora, toda enumeração alcançável a partir de Canvas é serializada pelo nome, sem conversor registrado em tempo de execução:

var json = JsonSerializer.Serialize(Canvas.Poster, CanvasContext.Default.Canvas);

A propriedade UseStringEnumConverter foi introduzida no .NET 8, então esse formato exige o .NET 8 ou posterior ou, em um framework de destino mais antigo, o pacote System.Text.Json 8.0 junto com o C# 12.

Quero que se aplique a...System.Text.JsonNewtonsoft.Json
uma propriedade[JsonConverter(typeof(JsonStringEnumConverter))] na propriedade[JsonConverter(typeof(StringEnumConverter))] na propriedade
todos os usos de um tipo de enumeraçãoo mesmo atributo na declaração enumo mesmo atributo na declaração enum
uma chamada do serializadoroptions.Converters.Add(new JsonStringEnumConverter())passar new StringEnumConverter() para SerializeObject
uma aplicação MVC / Web API inteiraAddControllers().AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(...))AddControllers().AddNewtonsoftJson(o => o.SerializerSettings.Converters.Add(...))
uma aplicação inteira com APIs mínimasbuilder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Converters.Add(...))Sem suporte nativo até o .NET 10

Como serializar uma enumeração com um valor de string personalizado?

Ao serializar uma enumeração como string, talvez seja preciso fazer alguns ajustes finos, por exemplo, converter para camelCase, expor um texto mais significativo etc. Vamos ver como fazer isso.

Serializar como texto em camelCase

Serializar JSON em camelCase é uma prática comum. Na verdade, esse é o comportamento padrão do ASP.NET Core. Mas, em geral, isso significa converter para camelCase os nomes das propriedades, e não os valores delas, e há um artigo separado sobre como converter também os nomes das propriedades para camelCase:

// 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"
  }
}

Isso é autoexplicativo e tecnicamente lógico, mas, na prática, pode não ser o desejado para enumerações. Por isso, o conversor de enumeração oferece uma forma de converter explicitamente os valores da enumeração para 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);

Basta especificar a política (ou estratégia) camelCase ao criar a instância do conversor de enumeração e passá-lo na serialização, como de costume:

{
  "Name": "Poster",
  "BackColor": "lightGray",
  "Medium": "water",
  "Pen": {
    "Name": "Simple",
    "Color": "red"
  }
}

Todos os valores da enumeração agora estão em camelCase. O conversor aceita qualquer política de nomenclatura, não só as nativas, então também podemos escrever um JsonNamingPolicy personalizado para a estratégia de nomenclatura e passá-lo no lugar.

Serializar como texto personalizado

Por causa das regras da linguagem para nomes de variáveis, nem sempre um membro de enumeração transmite um texto significativo quando serializado da forma comum. Para contornar esse problema, geralmente usamos EnumMemberAttribute, do assembly System.Runtime.Serialization, criado especificamente para esse fim:

using System.Runtime.Serialization;

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,
}

Definimos uma enumeração ToggleType com EnumMemberAttribute nos membros para fornecer valores mais significativos do que os nomes. Também definimos um record ToggleControl que usa essa enumeração.

O Newtonsoft respeita esse atributo diretamente:

// 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"
  }
]

Criamos um array de ToggleControl com valores diferentes de ToggleType. Na serialização, vemos que a saída traz os textos que queríamos!

The Web API Production Checklist, e-book gratuito em inglês

E-book gratuito

Sua Web API está pronta para produção?

33 itens para verificar antes de implantá-la, com a correção de cada um. Um PDF gratuito de 76 páginas para .NET 10.

O e-book está em inglês.

Baixe o checklist gratuito

PDF gratuito. Um único e-mail para enviá-lo. Cancele a inscrição quando quiser.

O System.Text.Json ignora [EnumMember], e sempre ignorou. Ele tem o próprio atributo para a mesma tarefa, JsonStringEnumMemberNameAttribute, adicionado no .NET 9:

public enum ToggleType
{
    [JsonStringEnumMemberName("Enable/Disable")]
    EnableDisable,

    [JsonStringEnumMemberName("Visible/Hidden")]
    VisibleHidden,
}

Com JsonStringEnumConverter registrado, essas são as strings que saem, e a desserialização as converte de volta no membro certo. Sem um conversor registrado, o atributo não faz absolutamente nada, e o mesmo modelo continua gerando {"Name":"toggle1","Type":0}; portanto, aqui o conversor não é opcional.

Antes de esse atributo existir, as opções eram uma política de nomenclatura personalizada ou um conversor escrito à mão, e ainda vale a pena conhecer o conversor para os casos que o atributo não cobre: veja como escrever um JsonConverter personalizado para o Newtonsoft.Json.

Como serializar uma enumeração com DataContractJsonSerializer?

O DataContractJsonSerializer fica em System.Runtime.Serialization.Json e é anterior ao System.Text.Json. É o serializador sobre o qual foram construídos os endpoints JSON do WCF e os endpoints ASP.NET AJAX, e ele continua disponível nativamente.

Ele não consegue serializar uma enumeração como string. Não há conversor para registrar nem configuração para alterar: todo membro de enumeração é escrito como o número subjacente.

[EnumMember] não muda isso. Decore um membro com [EnumMember(Value = "Enable/Disable")], e a saída continua sendo 0.

Essa é a surpresa, porque o DataContractSerializer, que gera XML, respeita esse atributo. A mesma enumeração, com o mesmo atributo, gera o nome quando escrita em XML e o número quando escrita em JSON.

A desserialização segue a mesma lógica. Desserializar um payload que contém a string lança uma exceção em vez de converter o valor, enquanto qualquer valor Int64 é aceito, mesmo um que nenhum membro define.

Então, se a saída precisa trazer o nome, esse é o serializador errado. O JsonStringEnumConverter e o StringEnumConverter fazem isso; este não tem equivalente.

Veja tudo em um único exemplo. No .NET 10, a classe não precisa de referência a pacote; ela vem do framework compartilhado:

global using System.Runtime.Serialization;
global using System.Runtime.Serialization.Json;
global using System.Text;

[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());

Os dois membros têm [EnumMember], e os dois saem como números:

{"Decorated":0,"Undecorated":1}

Esse é o comportamento documentado, e não um acidente. A referência da Microsoft sobre serialização JSON autônoma o inclui entre as regras do serializador: “Os atributos EnumMemberAttribute e NonSerializedAttribute são ignorados, se usados.”

Passar de volta a string do próprio atributo gera uma falha definitiva, e não um valor alternativo. Ler {"Decorated":"Enable/Disable","Undecorated":1} lança uma SerializationException dizendo que o valor não pode ser convertido no tipo Int64. Já um número que a enumeração nunca declara é desserializado sem reclamação.

Troque pelo DataContractSerializer de XML, e o mesmo tipo gera <Decorated>Enable/Disable</Decorated>. O atributo não é ignorado pelo modelo de contrato de dados, apenas pelo serializador JSON desse modelo.

Serialização JSON de enumerações de flags

A serialização padrão de enumerações de flags (enumerações de máscara de bits) produz uma saída ainda mais estranha. Em vez de aparecerem como uma combinação de flags, elas são expostas como um valor combinado. Para relembrar como o atributo [Flags] funciona, temos um artigo dedicado:

[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 });

A saída:

{"Format":7}

Começamos definindo uma enumeração de flags TextStyles. Em seguida, declaramos styles, que é uma combinação das flags Bold(1), Italic(2) e Underline(4). Na serialização padrão, poderíamos esperar pelo menos “1, 2, 4”. Mas obtemos 7, porque o serializador padrão só se importa com o valor numérico da enumeração.

A conversão de enumeração para string não tem essa estranheza:

var styles = TextStyles.Bold | TextStyles.Italic | TextStyles.Underline;
var json = SerializeWithStringEnum(new { Format = styles });

Agora, temos um resultado diferente:

{"Format":"Bold, Italic, Underline"}

Essa é a saída que queremos! A desserialização converte essa forma separada por vírgulas de volta na mesma combinação, então uma enumeração de flags sobrevive à ida e volta como string.

Conclusão

Neste artigo, vimos algumas formas de serializar uma enumeração como string em JSON. Também discutimos várias técnicas para obter uma serialização personalizada de enumerações como string.

Resumindo: registre JsonStringEnumConverter para o System.Text.Json ou StringEnumConverter para o Newtonsoft.Json e escolha o posicionamento adequado ao escopo desejado. O DataContractJsonSerializer é o único serializador que não consegue fazer isso. A conversão no sentido inverso, de texto para membro, é outra tarefa: veja como converter uma string ou um int de volta em uma enumeração.

Testado com .NET 10 e Newtonsoft.Json 13.0.4.