Menu

JSON em C#: serializar e desserializar com System.Text.Json

Como trabalhar com JSON em C# usando System.Text.Json: JsonSerializer.Serialize e Deserialize, saída em camelCase e indentada, atributos como JsonPropertyName e JsonIgnore, enums como strings, ler JSON sem classes com JsonDocument e JsonNode, erros e a comparação com o Newtonsoft.Json.

JSON é como a maioria dos programas C# conversa com APIs web, guarda configurações e troca dados. O .NET moderno o lê e escreve com System.Text.Json, que faz parte do runtime a partir do .NET Core 3.0: nenhum pacote NuGet é necessário. O ponto de entrada principal é a classe static JsonSerializer, que transforma objetos em strings JSON e vice-versa.

O System.Text.Json é embutido no .NET Core 3.0 e em todas as versões seguintes (do .NET 5 ao .NET 10). Projetos no .NET Framework 4.6.2 ou posterior, ou no .NET Standard 2.0, também podem usá-lo instalando o pacote NuGet System.Text.Json. Os exemplos desta página aparecem como código simples, com a saída em comentários. Adicione estas diretivas using para executá-los em um projeto .NET:

using System.Text.Json;
using System.Text.Json.Serialization;

Serializar: objeto para JSON

JsonSerializer.Serialize escreve todas as propriedades públicas de um objeto, usando os nomes das propriedades como estão:

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public List<string> Tags { get; set; } = new List<string>();
    public bool InStock { get; set; }
}

var lamp = new Product { Name = "Desk lamp", Price = 34.90m, Tags = { "home", "light" }, InStock = true };

string json = JsonSerializer.Serialize(lamp);
Console.WriteLine(json);
// {"Name":"Desk lamp","Price":34.90,"Tags":["home","light"],"InStock":true}

Coleções viram arrays, dicionários com chaves string viram objetos ({"apples":3,"pears":5}), null continua null e DateTime vira uma string ISO 8601 ("2026-03-01T14:30:00"). Dois padrões pegam muita gente:

  • Campos são ignorados. Só as propriedades são serializadas. Uma classe com public int X; é serializada como {}, a menos que você defina IncludeFields = true nas opções ou transforme o campo em propriedade.
  • Enums viram números. OrderStatus.Shipped é escrito como 1 (o valor subjacente do enum). Adicione JsonStringEnumConverter (abaixo) para escrever "Shipped".

Desserializar: JSON para objeto

JsonSerializer.Deserialize<T> cria um T e define as propriedades dele a partir do JSON:

string json = "{\"Name\":\"Desk lamp\",\"Price\":34.90,\"Tags\":[\"home\",\"light\"]}";
Product p = JsonSerializer.Deserialize<Product>(json);
Console.WriteLine($"{p.Name} {p.Price} {p.Tags.Count}");   // Desk lamp 34.90 2

var many = JsonSerializer.Deserialize<List<Product>>("[{\"Name\":\"A\",\"Price\":1},{\"Name\":\"B\",\"Price\":2.5}]");
Console.WriteLine(many.Count);                               // 2

Propriedades JSON sem propriedade C# correspondente são ignoradas, e propriedades C# sem JSON correspondente ficam com os valores padrão. Nenhum dos dois é um erro por padrão.

A regra que pega quase todo mundo: a correspondência de nomes diferencia maiúsculas de minúsculas. A maioria das APIs web envia camelCase, e camelCase não corresponde a propriedades em PascalCase:

string fromApi = "{\"name\":\"Mug\",\"price\":8.5}";

var a = JsonSerializer.Deserialize<Product>(fromApi);
Console.WriteLine($"[{a.Name}] {a.Price}");    // [] 0: nothing matched, no error

var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var b = JsonSerializer.Deserialize<Product>(fromApi, options);
Console.WriteLine($"[{b.Name}] {b.Price}");    // [Mug] 8.5

new JsonSerializerOptions(JsonSerializerDefaults.Web) dá as configurações que o ASP.NET Core usa: leitura sem diferenciar maiúsculas e minúsculas, escrita em camelCase e números aceitos como strings entre aspas.

A desserialização precisa de uma forma de definir cada valor: um setter público, um acessor init ou um construtor cujos nomes de parâmetros correspondam às propriedades. Essa última regra é o motivo de os records funcionarem sem configuração:

public record Point(int X, int Y);

Point pt = JsonSerializer.Deserialize<Point>("{\"X\":1,\"Y\":2}");
Console.WriteLine(pt);   // Point { X = 1, Y = 2 }

Opções: camelCase e saída indentada

JsonSerializerOptions controla os nomes, a formatação e mais. Crie uma instância e reutilize-a: o serializador guarda metadados em cache por instância de opções, então criar opções novas a cada chamada é mensuravelmente mais lento.

private static readonly JsonSerializerOptions Options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = true,
};

Console.WriteLine(JsonSerializer.Serialize(lamp, Options));
// {
//   "name": "Desk lamp",
//   "price": 34.90,
//   "tags": [
//     "home",
//     "light"
//   ],
//   "inStock": true
// }

Outras opções que vale conhecer: DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull para deixar de fora propriedades null, IncludeFields = true, NumberHandling para aceitar números escritos como strings, e ReadCommentHandling = JsonCommentHandling.Skip mais AllowTrailingCommas = true para arquivos de configuração editados à mão. O .NET 8 adicionou JsonNamingPolicy.SnakeCaseLower para APIs que usam snake_case.

Atributos: renomear, ignorar, enums como strings

Atributos na classe controlam uma propriedade por vez e têm prioridade sobre as opções:

public enum OrderStatus { Pending, Shipped }

public class Order
{
    [JsonPropertyName("order_id")]
    public int Id { get; set; }

    public string Customer { get; set; }

    [JsonIgnore]
    public string InternalNote { get; set; }                  // never written or read

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Coupon { get; set; }                        // left out when null

    [JsonConverter(typeof(JsonStringEnumConverter))]
    public OrderStatus Status { get; set; }

    public DateTime PlacedAt { get; set; }
}

var order = new Order
{
    Id = 1042, Customer = "Ana", InternalNote = "vip",
    Status = OrderStatus.Shipped, PlacedAt = new DateTime(2026, 3, 1, 14, 30, 0),
};
Console.WriteLine(JsonSerializer.Serialize(order));
// {"order_id":1042,"Customer":"Ana","Status":"Shipped","PlacedAt":"2026-03-01T14:30:00"}

Para escrever todos os enums como strings em vez de marcar cada propriedade, adicione o conversor às opções: options.Converters.Add(new JsonStringEnumConverter());.

Lendo JSON sem classe: JsonDocument e JsonNode

Quando você só precisa de alguns valores de uma resposta grande, ou o formato dela varia, dispense a classe. JsonDocument interpreta o texto como uma árvore somente leitura de valores JsonElement:

string weather = "{\"city\":\"Lisbon\",\"current\":{\"temp\":21.5,\"conditions\":[\"sunny\",\"windy\"]},\"alerts\":null}";

using (JsonDocument doc = JsonDocument.Parse(weather))
{
    JsonElement root = doc.RootElement;
    Console.WriteLine(root.GetProperty("city").GetString());                        // Lisbon
    Console.WriteLine(root.GetProperty("current").GetProperty("temp").GetDecimal()); // 21.5

    foreach (JsonElement c in root.GetProperty("current").GetProperty("conditions").EnumerateArray())
        Console.WriteLine(c.GetString());                                            // sunny, windy

    Console.WriteLine(root.TryGetProperty("humidity", out _));                       // False
    Console.WriteLine(root.GetProperty("alerts").ValueKind);                         // Null
}

GetProperty lança KeyNotFoundException para um nome que não existe, então use TryGetProperty para campos opcionais. JsonDocument usa memória emprestada de um pool, e é por isso que é descartado com using.

Para modificar JSON, use JsonNode, de System.Text.Json.Nodes (.NET 6 em diante), que dá uma árvore mutável com indexadores:

JsonNode node = JsonNode.Parse(weather);
Console.WriteLine((string)node["city"]);   // Lisbon

node["current"]["temp"] = 23;
node["updated"] = true;
Console.WriteLine(node.ToJsonString());
// {"city":"Lisbon","current":{"temp":23,"conditions":["sunny","windy"]},"alerts":null,"updated":true}

Arquivos e streams

JSON em disco é uma string em um arquivo, então os métodos de arquivo se combinam diretamente com o serializador:

File.WriteAllText("settings.json", JsonSerializer.Serialize(settings, Options));
var loaded = JsonSerializer.Deserialize<Settings>(File.ReadAllText("settings.json"), Options);
// Same options both ways: camelCase names written with Options would not match on a default read.

Para arquivos grandes e corpos HTTP, as sobrecargas async com stream evitam montar a string inteira na memória:

await using FileStream stream = File.OpenRead("orders.json");
List<Order> orders = await JsonSerializer.DeserializeAsync<List<Order>>(stream);

No ASP.NET Core e com HttpClient, você raramente chama o serializador você mesmo: os controllers fazem o binding dos corpos JSON automaticamente, e httpClient.GetFromJsonAsync<Order>(url) (em System.Net.Http.Json) faz a requisição e a desserialização em uma chamada.

Erros

JSON inválido, ou um valor que não pode ser convertido para o tipo da propriedade, lança JsonException. A mensagem dela informa o caminho JSON e a posição, o que normalmente basta para achar o problema:

try
{
    JsonSerializer.Deserialize<Product>("{\"Name\": \"Lamp\", \"Price\": \"cheap\"}");
}
catch (JsonException e)
{
    Console.WriteLine(e.Message);
    // The JSON value could not be converted to System.Decimal. Path: $.Price | LineNumber: 0 | BytePositionInLine: 33.
}

Deserialize retorna null (e não uma exceção) quando o texto JSON é o literal null, então verifique o resultado quando a entrada vier de fora.

Caracteres escapados na saída

Por padrão, o serializador escapa caracteres não ASCII e caracteres inseguros dentro de HTML:

Console.WriteLine(JsonSerializer.Serialize(new { city = "São Paulo", note = "5 > 3" }));
// {"city":"S\u00E3o Paulo","note":"5 \u003E 3"}

Isso é JSON válido e é lido de volta como o texto original; ele é escapado para que a saída possa ser colocada em uma página HTML com segurança. Para arquivos legíveis por pessoas, defina Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (de System.Text.Encodings.Web) nas opções. O "unsafe" se refere só a embutir o resultado em HTML.

System.Text.Json vs Newtonsoft.Json

O Newtonsoft.Json (Json.NET, a classe JsonConvert) foi o padrão por uma década e ainda está em todo lugar. As principais diferenças:

System.Text.JsonNewtonsoft.Json
Disponibilidadeembutido no .NET Core 3.0+; pacote NuGet para .NET Framework 4.6.2+pacote NuGet para .NET Framework e .NET
Serializar / desserializarJsonSerializer.Serialize(obj) / Deserialize<T>(json)JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json)
Correspondência de nomesdiferencia maiúsculas de minúsculas por padrãonão diferencia
Leitura sem tipoJsonDocument, JsonNodeJObject, JToken, com consultas JSONPath
Renomear uma propriedade[JsonPropertyName("x")][JsonProperty("x")]
Tolerânciarígido: sem comentários, vírgulas sobrando ou números entre aspas, a menos que habilitadostolerante por padrão
Desempenhomais rápido, menos alocações, source generation para trimming e AOTmais lento, baseado em reflection

Os atributos têm nomes e namespaces diferentes, então migrar uma base de código é um trabalho de buscar e substituir, mais testes para o parsing mais rígido. Para código .NET novo, comece com System.Text.Json.

Source generation (.NET 6+)

JsonSerializer normalmente inspeciona seus tipos com reflection em tempo de execução. Para apps com trimming ou compilados antecipadamente (Native AOT, Blazor WebAssembly), um source generator escreve esse código em tempo de compilação:

[JsonSerializable(typeof(Product))]
internal partial class AppJsonContext : JsonSerializerContext { }

string json = JsonSerializer.Serialize(lamp, AppJsonContext.Default.Product);

Erros comuns

  • JSON em camelCase para propriedades em PascalCase com as opções padrão. Todas as propriedades ficam vazias, sem aviso. Use PropertyNameCaseInsensitive ou JsonSerializerDefaults.Web.
  • Campos públicos em vez de propriedades. Eles não são serializados, a menos que IncludeFields esteja definido.
  • Propriedades sem setter. Uma propriedade só com get sem parâmetro de construtor correspondente não é preenchida na desserialização.
  • Um JsonSerializerOptions novo a cada chamada. Reutilize uma instância static.
  • Dinheiro em ponto flutuante. Um double de 0.1 + 0.2 é serializado como 0.30000000000000004. Use decimal para valores.

Perguntas frequentes

Como converter um objeto para JSON em C#?

Chame JsonSerializer.Serialize(obj) de System.Text.Json, que vem embutido no .NET Core 3.0 em diante, sem pacote para instalar. Ele escreve todas as propriedades públicas: {"Name":"Desk lamp","Price":34.90}. Passe new JsonSerializerOptions { WriteIndented = true } para uma saída legível e PropertyNamingPolicy = JsonNamingPolicy.CamelCase para nomes em camelCase.

Como converter JSON para um objeto em C#?

var product = JsonSerializer.Deserialize<Product>(json); cria um Product e preenche as propriedades públicas com setter a partir dos nomes JSON correspondentes. Por padrão, a correspondência diferencia maiúsculas de minúsculas, então JSON em camelCase deixa vazias as propriedades em PascalCase, a menos que você passe PropertyNameCaseInsensitive = true ou new JsonSerializerOptions(JsonSerializerDefaults.Web). JSON malformado ou um valor do tipo errado lança JsonException.

Como ler JSON sem criar uma classe em C#?

Use JsonDocument.Parse(json) e percorra RootElement com GetProperty("name"), GetString(), GetInt32() e EnumerateArray(); ele é somente leitura e rápido, e precisa ser descartado. Para JSON que você quer modificar, JsonNode.Parse(json) (.NET 6+) dá uma árvore mutável: node["city"], atribuições e ToJsonString().

Devo usar System.Text.Json ou Newtonsoft.Json?

Para código novo no .NET Core 3.0 ou posterior, System.Text.Json: ele é embutido, mais rápido, aloca menos e o ASP.NET Core o usa por padrão. O Newtonsoft.Json (Json.NET) continua comum em projetos .NET Framework (onde o System.Text.Json só está disponível como pacote NuGet), em código que depende dos recursos extras dele (consultas JSONPath, parsing muito tolerante, TypeNameHandling) e em bases de código grandes já construídas sobre ele.

Por que o System.Text.Json escapa caracteres como é e <?

O codificador padrão escapa caracteres não ASCII e caracteres sensíveis em HTML (<, >, &, ') como \uXXXX, para que a saída possa ser embutida em HTML com segurança. Continua sendo JSON válido e é desserializado de volta para o mesmo texto. Para uma saída legível, defina Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping nas opções, mas só quando o JSON não for escrito dentro de HTML.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR