C# JSON

Gravar texto num arquivo você já sabe. Mas como gravar um objeto, um contato com nome, cidade e telefone, ou uma lista com cem deles, e depois ler de volta exatamente igual? O formato mais usado do mundo para isso é o JSON. É nele que os sites conversam com os servidores, que os aplicativos guardam configurações e que muitas lojas trocam dados. Nesta aula você vai usar o System.Text.Json, que já vem no .NET, para transformar objetos em JSON e JSON em objetos, e vai salvar a agenda de contatos da Ana num arquivo.

Você vai aprender
  • Ler e escrever o formato JSON: objetos, listas, textos, números, true, false e null.
  • Transformar um objeto em JSON com JsonSerializer.Serialize (serializar), bonito com WriteIndented.
  • Transformar JSON em objeto com JsonSerializer.Deserialize<T> (desserializar).
  • Salvar e carregar uma lista num arquivo .json.
  • Controlar os nomes: camelCase, PropertyNameCaseInsensitive, [JsonPropertyName] e [JsonIgnore].
  • Ler um JSON sem criar classe, com JsonNode.
  • Os erros: JsonException, nomes que não batem e a reflexão desativada no dotnet run arquivo.cs.

Esta aula usa records, propriedades, List<T>, generics (o <T>) e a aula Arquivos.

Nesta aula, a Ana usa um projeto

No .NET 10, o dotnet run arquivo.cs (de um arquivo só) vem preparado para virar um programa "nativo" (AOT, seção "Quando dá errado"), e nesse modo o JsonSerializer simples é desligado. Por isso, os exemplos desta aula rodam num projeto, criado com dotnet new console -o Agenda (aula Projetos). O código vai no Program.cs e roda com dotnet run. Nas caixas "Experimente", o record fica numa aba à parte, para a caixa também funcionar como um projeto. (O editor do guia segue a mesma regra do dotnet: uma caixa com um .cs só é um arquivo solto e dá o mesmo erro; com duas abas .cs, é um projeto.)

$ dotnet new console -o Agenda
O modelo "Aplicativo do Console" foi criado com êxito.

Processando ações pós-criação...
Restaurando /home/ana/projetos/Agenda/Agenda.csproj:
  Determinando os projetos a serem restaurados...
  /home/ana/projetos/Agenda/Agenda.csproj restaurado (em 108 ms).
A restauração foi bem-sucedida.

$ cd Agenda

A ideia: a ficha que qualquer um preenche

Um objeto vive na memória do seu programa, num formato que só o .NET entende. Para guardá-lo num arquivo, ou mandá-lo para outro programa (que pode nem ser em C#), você precisa escrevê-lo numa ficha padronizada, que qualquer um sabe ler e preencher. O JSON (JavaScript Object Notation, "notação de objetos do JavaScript") é essa ficha. Ele é só texto:

{
  "Nome": "Bruno",
  "Cidade": "Curitiba",
  "Idade": 34,
  "Cliente": true,
  "Apelido": null,
  "Telefones": ["(41) 99999-0000", "(41) 3333-0000"]
}
No JSONExemploEm C#
Objeto: pares "nome": valor entre { }, separados por vírgula{ "Nome": "Bruno" }Um objeto de uma classe ou record
Texto, sempre entre aspas duplas"Curitiba"string
Número, com ponto nos decimais, em qualquer país34, 15.90int, double, decimal…
Verdadeiro ou falsotrue, falsebool
Nadanullnull
Lista (array) entre [ ]["a", "b"]array, List<T>

Transformar um objeto em JSON se chama serializar (pôr "em série", numa fila de letras). O caminho de volta, de JSON para objeto, é desserializar. Os dois ficam na classe JsonSerializer, do namespace System.Text.Json, que precisa de using.


1. Objeto → JSON: Serialize

using System.Text.Json;

Contato ana = new Contato("Ana Souza", "ana@exemplo.com", 29, ["Recife", "São Paulo"]);

string json = JsonSerializer.Serialize(ana);
Console.WriteLine(json);

JsonSerializerOptions opcoes = new JsonSerializerOptions { WriteIndented = true };
Console.WriteLine(JsonSerializer.Serialize(ana, opcoes));

record Contato(string Nome, string Email, int Idade, List<string> Cidades);
$ dotnet run
{"Nome":"Ana Souza","Email":"ana@exemplo.com","Idade":29,"Cidades":["Recife","São Paulo"]}
{
  "Nome": "Ana Souza",
  "Email": "ana@exemplo.com",
  "Idade": 29,
  "Cidades": [
    "Recife",
    "São Paulo"
  ]
}
using System.Text.Json;
Traz o JsonSerializer e o JsonSerializerOptions. Esse namespace não vem incluído sozinho.
record Contato(…)
Um record com quatro propriedades. Fica no fim do arquivo, depois das instruções do programa. Uma classe com propriedades { get; set; } funciona do mesmo jeito.
JsonSerializer.Serialize(ana)
Olha as propriedades públicas do objeto e escreve cada uma como "Nome": valor. A lista virou um array JSON. O resultado é uma string comum, que você pode mostrar, gravar num arquivo ou mandar pela internet.
new JsonSerializerOptions { WriteIndented = true }
As opções ("configurações") do serializador. WriteIndented ("escrever com recuo") põe cada valor numa linha, com recuo: mais fácil para pessoas lerem. Sem ele, tudo sai numa linha só, menor, melhor para programas.
São Paulo
O ã saiu como ã, o código Unicode da letra. É JSON válido, e quem ler o arquivo recebe São Paulo de volta. A seção 4 mostra como deixar o acento visível.

2. JSON → objeto: Deserialize

O caminho de volta. O Deserialize precisa saber em que tipo transformar o texto, e você diz isso entre < >:

using System.Text.Json;

string json = File.ReadAllText("contato.json");
Contato? c = JsonSerializer.Deserialize<Contato>(json);
if (c != null)
{
    Console.WriteLine($"{c.Nome} mora em {c.Cidade}");
}

record Contato(string Nome, string Cidade, string Telefone);
$ cat contato.json
{
  "Nome": "Bruno",
  "Cidade": "Curitiba",
  "Telefone": "(41) 99999-0000"
}
$ dotnet run
Bruno mora em Curitiba
JsonSerializer.Deserialize<Contato>(json)
Lê o texto e cria um Contato, preenchendo cada propriedade com o valor do mesmo nome no JSON.
Contato?
O resultado pode ser null: se o texto for literalmente null, é isso que volta. Por isso a variável é Contato? e o if (c != null) confere antes de usar (aula Referências Anuláveis).

O JSON é lido com a mesma regra de nomes do Serialize: "Nome" no texto vai para Nome no objeto. Valores do JSON que não existem no record são ignorados, e propriedades do record que não aparecem no JSON ficam com o valor padrão (null, 0, false).

Missão: o arquivo contato.json (na outra aba) tem a ficha da Carla. Transforme o texto num Contato com JsonSerializer.Deserialize<Contato>(json) e mostre Carla mora em Salvador.

using System.Text.Json;

string json = File.ReadAllText("contato.json");
Console.WriteLine(json);
// transforme o json num Contato e mostre "Nome mora em Cidade"
record Contato(string Nome, string Cidade, string Telefone);
{
  "Nome": "Carla",
  "Cidade": "Salvador",
  "Telefone": "(71) 97777-2222"
}
using System.Text.Json;

string json = File.ReadAllText("contato.json");
Contato? c = JsonSerializer.Deserialize<Contato>(json);
if (c != null)
{
    Console.WriteLine($"{c.Nome} mora em {c.Cidade}");
}

Nas caixas desta aula, o record fica numa aba própria, Contato.cs. Com dois arquivos .cs, a caixa funciona como um projeto, igual ao ~/projetos/Agenda da Ana (no projeto, ele também pode ficar num arquivo separado; todos os .cs da pasta são compilados juntos).


3. Salvar e carregar uma lista

A agenda da Ana é uma List<Contato>. O JSON de uma lista é um array de objetos, e o Deserialize aceita o tipo List<Contato> do mesmo jeito. Juntando com o File, a agenda passa a sobreviver ao fim do programa:

using System.Text.Json;

JsonSerializerOptions opcoes = new JsonSerializerOptions { WriteIndented = true };

string texto = File.ReadAllText("contatos.json");
List<Contato> agenda = JsonSerializer.Deserialize<List<Contato>>(texto) ?? [];

agenda.Add(new Contato("Carla", "Salvador", "(71) 97777-2222"));
File.WriteAllText("contatos.json", JsonSerializer.Serialize(agenda, opcoes));

Console.WriteLine($"{agenda.Count} contatos");
Console.WriteLine(File.ReadAllText("contatos.json"));

record Contato(string Nome, string Cidade, string Telefone);
$ dotnet run
3 contatos
[
  {
    "Nome": "Ana",
    "Cidade": "Recife",
    "Telefone": "(81) 98888-1111"
  },
  {
    "Nome": "Bruno",
    "Cidade": "Curitiba",
    "Telefone": "(41) 99999-0000"
  },
  {
    "Nome": "Carla",
    "Cidade": "Salvador",
    "Telefone": "(71) 97777-2222"
  }
]
Deserialize<List<Contato>>(texto)
O tipo agora é "lista de contatos". O array do arquivo, que tinha a Ana e o Bruno, virou uma lista com dois itens.
?? []
Se o resultado for null (o arquivo tinha só null), use uma lista vazia (operador ??, aula Operadores ??, ?. e ??=). Assim agenda é List<Contato>, sem ?, e o resto do programa não precisa conferir.
File.WriteAllText("contatos.json", JsonSerializer.Serialize(agenda, opcoes))
Serializa a lista inteira e substitui o arquivo, já com a Carla.

Esse é o jeito mais simples de um programa pequeno "lembrar" das coisas: carregar tudo no começo, mexer na lista e salvar tudo no fim. O projeto Agenda de Contatos faz um programa completo assim.


4. Nomes, acentos e números

Muitos sites e programas escrevem JSON em camelCase ("nome", "precoUnitario"), e o C# usa PascalCase nas propriedades (Nome, PrecoUnitario). As opções e dois atributos resolvem a diferença:

using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;

JsonSerializerOptions opcoes = new JsonSerializerOptions
{
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};

List<Produto> estoque =
[
    new Produto { Nome = "Caderno", Preco = 15.90m, Quantidade = 12 },
    new Produto { Nome = "Caneta azul", Preco = 2.50m, Quantidade = 40, Senha = "x" },
];
Console.WriteLine(JsonSerializer.Serialize(estoque, opcoes));

class Produto
{
    public string Nome { get; set; } = "";
    public decimal Preco { get; set; }
    [JsonPropertyName("qtd")]
    public int Quantidade { get; set; }
    [JsonIgnore]
    public string? Senha { get; set; }
}
$ dotnet run
[
  {
    "nome": "Caderno",
    "preco": 15.90,
    "qtd": 12
  },
  {
    "nome": "Caneta azul",
    "preco": 2.50,
    "qtd": 40
  }
]
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
"Política de nomes": escreve Nome como "nome" e Preco como "preco". Use as mesmas opções no Deserialize, para a volta entender os nomes.
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
Deixa acentos como ã e símbolos como & sem virar ã. O "Unsafe" ("inseguro") é porque esse JSON não deve ser colado dentro de uma página HTML sem cuidado; para arquivos e programas, é tranquilo. Fica em System.Text.Encodings.Web.
[JsonPropertyName("qtd")]
Um atributo (de System.Text.Json.Serialization) que escolhe o nome exato no JSON só para essa propriedade. Vale nos dois sentidos.
[JsonIgnore]
"Ignorar": a propriedade não vai para o JSON (nem é lida dele). Bom para senhas e valores calculados.
"preco": 15.90
No JSON, o número sai sempre com ponto, mesmo no computador em pt-BR da Ana. O JSON não depende da cultura: não há o problema do 8.5 que vira 85. E o decimal manteve os dois zeros.

Ler sem diferenciar maiúsculas

Ao ler um JSON escrito por outro programa, você nem sempre sabe se os nomes vêm como "nome", "Nome" ou "NOME". A opção PropertyNameCaseInsensitive = true ("nomes sem diferenciar maiúsculas") aceita todos. Este trecho usa a mesma classe Produto (no fim do arquivo, como antes):

string deFora = """{"nome":"Régua","preco":1.2,"cor":"azul"}""";

Produto? r1 = JsonSerializer.Deserialize<Produto>(deFora);
Console.WriteLine($"Sem opções: [{r1?.Nome}] {r1?.Preco}");

JsonSerializerOptions flexivel = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
Produto? r2 = JsonSerializer.Deserialize<Produto>(deFora, flexivel);
Console.WriteLine($"Sem diferenciar maiúsculas: [{r2?.Nome}] {r2?.Preco}");
$ dotnet run
Sem opções: [] 0
Sem diferenciar maiúsculas: [Régua] 1,2

Sem a opção, nenhum nome bateu ("nome" não é Nome) e o produto veio vazio, sem nenhum erro. Com ela, deu certo. O "cor", que não existe na classe, foi ignorado nos dois casos. O """…""" é a string literal crua da aula Escapes, que deixa escrever aspas sem barra.


5. Ler sem criar classe: JsonNode

Às vezes você só quer um ou dois valores de um JSON grande, e criar classes para tudo dá trabalho. O JsonNode ("nó JSON", de System.Text.Json.Nodes) lê o JSON como uma árvore, que você percorre com ["nome"] e [0]:

using System.Text.Json.Nodes;

string json = """
{
  "loja": "Papelaria da Ana",
  "aberta": true,
  "produtos": [
    { "nome": "Caderno", "preco": 15.90 },
    { "nome": "Caneta", "preco": 2.50 }
  ]
}
""";
JsonNode? raiz = JsonNode.Parse(json);
Console.WriteLine(raiz?["loja"]);
Console.WriteLine((bool?)raiz?["aberta"]);

JsonArray produtos = raiz!["produtos"]!.AsArray();
Console.WriteLine($"{produtos.Count} produtos");
foreach (JsonNode? p in produtos)
{
    string nome = (string)p!["nome"]!;
    decimal preco = (decimal)p["preco"]!;
    Console.WriteLine($"{nome}: {preco:C}");
}
$ dotnet run
Papelaria da Ana
True
2 produtos
Caderno: R$ 15,90
Caneta: R$ 2,50
JsonNode.Parse(json)
"Analisa" o texto e devolve a raiz da árvore (ou null, se o texto for null).
raiz?["loja"]
O valor do campo "loja". Se o campo não existir, vem null, sem exceção.
(bool?)…, (string)…, (decimal)…
Um cast converte o nó no tipo C# que você quer.
.AsArray()
Trata o nó como lista, com Count e foreach.
! (depois de um valor)
O operador "eu garanto que não é null" (aula Referências Anuláveis). Aqui você sabe que os campos existem; num JSON de fora, prefira conferir.

Qual usar

Você quer…Use
Guardar e carregar os seus próprios objetosJsonSerializer.Serialize e Deserialize<T> com um record ou classe
Um arquivo que pessoas vão lerWriteIndented = true e o Encoder que mantém os acentos
Conversar com um site que usa "nomeAssim"PropertyNamingPolicy = JsonNamingPolicy.CamelCase (nos dois sentidos)
Ler JSON de fora, sem saber as maiúsculasPropertyNameCaseInsensitive = true
Um nome especial numa propriedade[JsonPropertyName("…")]
Pegar alguns valores de um JSON grandeJsonNode.Parse
Programa nativo (AOT) ou dotnet run arquivo.csGeração de código com JsonSerializerContext (abaixo)

Quando dá errado

Reflection-based serialization has been disabled no dotnet run arquivo.cs

A Ana tentou o primeiro exemplo da aula num arquivo solto, ~/cs/json1.cs, como nas aulas dos níveis 1 e 2:

$ dotnet run json1.cs
/home/ana/cs/json1.cs(4,15): warning IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.
/home/ana/cs/json1.cs(4,15): warning IL3050: Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.
…                                                « o mesmo par de avisos para cada Serialize e Deserialize
Unhandled exception. System.InvalidOperationException: Reflection-based serialization has been disabled for this application. Either use the source generator APIs or explicitly configure the 'JsonSerializerOptions.TypeInfoResolver' property.
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_JsonSerializerIsReflectionDisabled()
   …                                                « mais 4 linhas de dentro do .NET
   at Program.<Main>$(String[] args) in /home/ana/cs/json1.cs:line 4

O que aconteceu: no .NET 10, um programa de arquivo único vem configurado para ser publicado como programa nativo (AOT, "compilado antes", aula Publicar o Programa). Um programa nativo não pode examinar as classes enquanto roda (isso se chama reflexão), e é assim que o JsonSerializer simples descobre as propriedades. Os avisos IL2026 e IL3050 avisam disso, e a exceção diz "a serialização por reflexão foi desativada para este programa". Num projeto (dotnet new console), essa configuração vem desligada, e tudo funciona.

Como resolver: três caminhos.

  1. Use um projeto, como nesta aula.
  2. No arquivo solto, desligue o modo nativo com uma diretiva na primeira linha: #:property PublishAot=false. Os avisos somem e o programa roda.
  3. Use a geração de código, que funciona em todo lugar, inclusive em programas nativos: uma classe partial marcada com [JsonSerializable], que o compilador completa sozinho.
#:property PublishAot=false
using System.Text.Json;

Contato ana = new Contato("Ana Souza", "ana@exemplo.com", 29, ["Recife", "São Paulo"]);
Console.WriteLine(JsonSerializer.Serialize(ana));

record Contato(string Nome, string Email, int Idade, List<string> Cidades);
using System.Text.Json;
using System.Text.Json.Serialization;

Contato ana = new Contato("Ana Souza", "ana@exemplo.com", 29, ["Recife", "São Paulo"]);
string json = JsonSerializer.Serialize(ana, AgendaJson.Default.Contato);
Console.WriteLine(json);
Contato? volta = JsonSerializer.Deserialize(json, AgendaJson.Default.Contato);
Console.WriteLine(volta?.Nome);

record Contato(string Nome, string Email, int Idade, List<string> Cidades);

[JsonSerializable(typeof(Contato))]
partial class AgendaJson : JsonSerializerContext { }
$ dotnet run json2.cs
{"Nome":"Ana Souza","Email":"ana@exemplo.com","Idade":29,"Cidades":["Recife","São Paulo"]}
$ dotnet run json3.cs
{"Nome":"Ana Souza","Email":"ana@exemplo.com","Idade":29,"Cidades":["Recife","São Paulo"]}
Ana Souza

No json3.cs, o AgendaJson.Default.Contato é a "receita" para ler e escrever um Contato, que o compilador gerou ao ver o [JsonSerializable(typeof(Contato))]. Para uma lista, acrescente outro atributo: [JsonSerializable(typeof(List<Contato>))].

O editor do guia faz igual ao terminal: o json1.cs numa caixa de um arquivo só dá os mesmos avisos IL2026/IL3050 e a mesma exceção, e o json2.cs e o json3.cs funcionam. A geração de código é o caminho que funciona em todo lugar: arquivo solto, projeto, programa nativo e editor. Experimente:

Missão: esta caixa tem um arquivo só, como o dotnet run lista.cs, e usa a geração de código. Mas o AgendaJson só sabe escrever um Contato, e o programa quer escrever a lista inteira: rode e veja o erro CS1061 (não existe ListContato). Acrescente, logo acima do partial class, o atributo [JsonSerializable(typeof(List<Contato>))] e rode de novo: o compilador cria a receita AgendaJson.Default.ListContato.

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

List<Contato> agenda = [new("Ana", "Recife"), new("Bruno", "Natal")];
string json = JsonSerializer.Serialize(agenda, AgendaJson.Default.ListContato);
Console.WriteLine(json);

record Contato(string Nome, string Cidade);

[JsonSerializable(typeof(Contato))]
partial class AgendaJson : JsonSerializerContext { }
using System.Text.Json;
using System.Text.Json.Serialization;

List<Contato> agenda = [new("Ana", "Recife"), new("Bruno", "Natal")];
string json = JsonSerializer.Serialize(agenda, AgendaJson.Default.ListContato);
Console.WriteLine(json);

record Contato(string Nome, string Cidade);

[JsonSerializable(typeof(Contato))]
[JsonSerializable(typeof(List<Contato>))]
partial class AgendaJson : JsonSerializerContext { }
JsonException: o valor não é do tipo certo
using System.Text.Json;

string json = """{"Nome":"Ana", "Idade": "vinte e nove"}""";
Pessoa? p = JsonSerializer.Deserialize<Pessoa>(json);
Console.WriteLine(p?.Nome);

record Pessoa(string Nome, int Idade);
$ dotnet run
Unhandled exception. System.Text.Json.JsonException: The JSON value could not be converted to Pessoa. Path: $.Idade | LineNumber: 0 | BytePositionInLine: 38.
 ---> System.InvalidOperationException: Cannot get the value of a token type 'String' as a number.
   at System.Text.Json.ThrowHelper.ThrowInvalidOperationException_ExpectedNumber(JsonTokenType tokenType)
   …                                                « mais 14 linhas de dentro do .NET
   at Program.<Main>$(String[] args) in /home/ana/projetos/Agenda/Program.cs:line 4

O que aconteceu: "O valor JSON não pôde ser convertido". A Idade é int, mas no JSON veio um texto (entre aspas). A mensagem é ótima para achar o problema: Path: $.Idade é o campo ($ é a raiz), e LineNumber e BytePositionInLine dizem a linha e a posição no texto, contando do zero.

Como resolver: conserte o JSON ("Idade": 29, sem aspas) ou, se o JSON vem de fora, trate com try { … } catch (JsonException e) { Console.WriteLine(e.Message); }.

JsonException: JSON mal escrito
$ grep json Program.cs
string json = """{"Nome":"Ana", "Idade": 29,}""";
$ dotnet run
Unhandled exception. System.Text.Json.JsonException: The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options. Path: $ | LineNumber: 0 | BytePositionInLine: 27.
 ---> System.Text.Json.JsonReaderException: The JSON object contains a trailing comma at the end which is not supported in this mode. Change the reader options. LineNumber: 0 | BytePositionInLine: 27.
   …                                                « mais 14 linhas de dentro do .NET
   at Program.<Main>$(String[] args) in /home/ana/projetos/Agenda/Program.cs:line 4

O que aconteceu: "O objeto JSON tem uma vírgula sobrando no fim". O JSON é mais rígido que o C#: nada de vírgula depois do último item, nada de comentários, nada de aspas simples ('Ana'), e os nomes sempre entre aspas duplas.

Como resolver: tire a vírgula. Se o JSON vem de um lugar que escreve assim, as opções AllowTrailingCommas = true e ReadCommentHandling = JsonCommentHandling.Skip deixam o leitor mais tolerante.

Os valores vieram vazios, sem erro nenhum
using System.Text.Json;

Pessoa? p = JsonSerializer.Deserialize<Pessoa>("""{"nome":"Ana","idade":29}""");
Console.WriteLine($"{p?.Nome} tem {p?.Idade} anos");

record Pessoa(string Nome, int Idade);
$ dotnet run
 tem 0 anos

O que aconteceu: o JSON usa "nome" e "idade", minúsculos, e o record usa Nome e Idade. Por padrão, o Deserialize diferencia maiúsculas, não achou nenhum campo e deixou os valores padrão: null e 0. Campos que não batem nunca dão erro.

Como resolver: passe new JsonSerializerOptions { PropertyNameCaseInsensitive = true }, ou PropertyNamingPolicy = JsonNamingPolicy.CamelCase (seção 4). Para exigir um campo, marque a propriedade com [JsonRequired]: aí a falta dele lança JsonException.

Avisos CS8600 e CS8602: o resultado pode ser null
$ grep Deserialize Program.cs
Pessoa p = JsonSerializer.Deserialize<Pessoa>("""{"Nome":"Ana","Idade":29}""");
$ dotnet run
/home/ana/projetos/Agenda/Program.cs(3,12): warning CS8600: Conversão de literal nula ou possível valor nulo em tipo não anulável. [/home/ana/projetos/Agenda/Agenda.csproj]
/home/ana/projetos/Agenda/Program.cs(4,22): warning CS8602: Desreferência de uma referência possivelmente nula. [/home/ana/projetos/Agenda/Agenda.csproj]
Ana tem 29 anos

O que aconteceu: o Deserialize devolve Pessoa?, e a variável foi declarada como Pessoa. Num projeto, o aviso vem com o nome do .csproj entre colchetes no fim.

Como resolver: Pessoa? p = … e confira com if (p != null), ou dê um valor para o caso null com ??, como em ?? [] para listas.


Referência rápida

CódigoO que faz
using System.Text.Json;Traz JsonSerializer e JsonSerializerOptions
JsonSerializer.Serialize(obj)Objeto → texto JSON
JsonSerializer.Serialize(obj, opcoes)Com opções
JsonSerializer.Deserialize<T>(json)Texto JSON → objeto (T?)
new JsonSerializerOptions { WriteIndented = true }Uma linha por valor, com recuo
PropertyNamingPolicy = JsonNamingPolicy.CamelCaseNomes "assim"
PropertyNameCaseInsensitive = trueLer sem diferenciar maiúsculas
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscapingAcentos visíveis (System.Text.Encodings.Web)
[JsonPropertyName("x")], [JsonIgnore], [JsonRequired]Nome próprio, fora do JSON, obrigatório (System.Text.Json.Serialization)
JsonNode.Parse(json)?["campo"]Ler sem classe (System.Text.Json.Nodes)
[JsonSerializable(typeof(T))] partial class X : JsonSerializerContext { }Geração de código, para AOT e arquivos soltos
#:property PublishAot=falseNo dotnet run arquivo.cs, liga de novo a reflexão

Exercícios

1. Para JSON

Complete para transformar o objeto produto num texto JSON:

string json = JsonSerializer.(produto);

2. De volta

Complete para transformar o texto numa lista de produtos:

List<Produto>? lista = JsonSerializer.Deserialize<>(json);

3. Bonito para ler

Complete a opção que põe cada valor numa linha, com recuo:

new JsonSerializerOptions { = true }

4. Num computador em pt-BR, como o JsonSerializer escreve o decimal 2.50m?

5. O JSON tem "nome": "Ana" e a classe tem Nome. Sem opções, o que o Deserialize faz?

6. Qual destes textos é um JSON válido?

Missão final: a agenda que lembra

Missão: o programa carrega a agenda do contatos.json (na outra aba) e mostra quantos contatos ela tem. Complete-o:

  1. acrescente na lista o contato new Contato("Carla", "Salvador", "(71) 97777-2222");
  2. serialize a lista com WriteIndented = true e grave em contatos.json com File.WriteAllText;
  3. mostre a quantidade (3 contatos) e depois o arquivo, lido de volta com File.ReadAllText.
using System.Text.Json;

string texto = File.ReadAllText("contatos.json");
List<Contato> agenda = JsonSerializer.Deserialize<List<Contato>>(texto) ?? [];

// 1. acrescente a Carla
// 2. grave a lista, com recuo, em contatos.json

Console.WriteLine($"{agenda.Count} contatos");
// 3. mostre o arquivo
record Contato(string Nome, string Cidade, string Telefone);
[
  {
    "Nome": "Ana",
    "Cidade": "Recife",
    "Telefone": "(81) 98888-1111"
  },
  {
    "Nome": "Bruno",
    "Cidade": "Curitiba",
    "Telefone": "(41) 99999-0000"
  }
]
using System.Text.Json;

string texto = File.ReadAllText("contatos.json");
List<Contato> agenda = JsonSerializer.Deserialize<List<Contato>>(texto) ?? [];

agenda.Add(new Contato("Carla", "Salvador", "(71) 97777-2222"));
JsonSerializerOptions opcoes = new JsonSerializerOptions { WriteIndented = true };
File.WriteAllText("contatos.json", JsonSerializer.Serialize(agenda, opcoes));

Console.WriteLine($"{agenda.Count} contatos");
Console.WriteLine(File.ReadAllText("contatos.json"));

No seu computador, rodando o programa de novo, a agenda carrega os 3 contatos do arquivo e grava 4: ela "lembra" entre uma execução e outra. Se quiser ir além: antes de acrescentar, confira se já existe alguém com o mesmo nome (agenda.Any(c => c.Nome == "Carla"), aula LINQ).


Resumo

  • JSON é texto: objetos { "nome": valor }, listas [ ], textos entre aspas duplas, números com ponto, true, false e null.
  • JsonSerializer.Serialize(obj) transforma as propriedades públicas em JSON; Deserialize<T>(json) faz o caminho de volta e devolve T?.
  • Para salvar e carregar: File.WriteAllText(c, JsonSerializer.Serialize(lista, opcoes)) e JsonSerializer.Deserialize<List<T>>(File.ReadAllText(c)) ?? [].
  • As opções controlam o formato (WriteIndented), os nomes (CamelCase, PropertyNameCaseInsensitive) e os acentos (Encoder). Use as mesmas na ida e na volta.
  • Nomes que não batem deixam valores vazios, sem erro. JSON mal escrito ou com tipo errado lança JsonException, com o campo e a posição.
  • No dotnet run arquivo.cs, a serialização por reflexão vem desligada: use um projeto, #:property PublishAot=false ou a geração de código com JsonSerializerContext.