System.Text.Json viene con .NET y convierte objetos en JSON y al revés sin instalar nada. Funciona en una línea, pero tiene un comportamiento por omisión que confunde a casi todo el mundo la primera vez: las mayúsculas.
También te puede interesar
De objeto a JSON
.NET trae su propia biblioteca de JSON, System.Text.Json, rápida y sin dependencias. Con un record para describir los datos, convertirlos es una línea:
#:property PublishAot=false
using System.Text.Json;
var libro = new Libro("Cien años de soledad", "Gabriel García Márquez", 1967, ["novela", "realismo mágico"]);
Console.WriteLine(JsonSerializer.Serialize(libro));
var opciones = new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true };
Console.WriteLine(JsonSerializer.Serialize(libro, opciones));
record Libro(string Titulo, string Autor, int Anio, List<string> Etiquetas);salida{"Titulo":"Cien a\u00F1os de soledad","Autor":"Gabriel Garc\u00EDa M\u00E1rquez","Anio":1967,"Etiquetas":["novela","realismo m\u00E1gico"]}
{
"titulo": "Cien a\u00F1os de soledad",
"autor": "Gabriel Garc\u00EDa M\u00E1rquez",
"anio": 1967,
"etiquetas": [
"novela",
"realismo m\u00E1gico"
]
}La primera línea, #:property PublishAot=false, solo hace falta en un archivo suelto; la explicamos en la sección siguiente. Por omisión, Serialize escribe los nombres de las propiedades tal cual, en PascalCase, y todo en una línea. Las opciones JsonSerializerDefaults.Web los pasan a camelCase, que es lo habitual en las API web; WriteIndented lo deja legible. Los acentos se escapan (\u00F1) por seguridad; si el JSON no va a una página web, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping los deja como están.
Archivos sueltos: AOT viene activado
Desde .NET 10, dotnet run archivo.cs compila los archivos sueltos con AOT nativo activado por omisión. Y con AOT, System.Text.Json desactiva la conversión por reflexión, que es la que usa cuando le pasas un tipo cualquiera. Sin la directiva, el compilador ya avisa (avisos IL2026 e IL3050), y al ejecutar:
using System.Text.Json;
Console.WriteLine(JsonSerializer.Serialize(new { Nombre = "Ana" }));salida con errorUnhandled 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.
Hay dos salidas. La sencilla es la de los ejemplos de esta página: desactivar AOT con #:property PublishAot=false en la primera línea. La otra, la que conviene si de verdad vas a compilar a nativo, es declarar un JsonSerializerContext (al final de la página). En un proyecto creado con dotnet new console AOT no está activado y nada de esto hace falta.
De JSON a objeto, y la trampa de las mayúsculas
#:property PublishAot=false
using System.Text.Json;
string json = """
{"titulo": "Rayuela", "autor": "Julio Cortázar", "anio": 1963}
""";
var tal_cual = JsonSerializer.Deserialize<Ficha>(json)!;
Console.WriteLine($"Sin opciones: '{tal_cual.Titulo}', {tal_cual.Anio}");
var web = new JsonSerializerOptions(JsonSerializerDefaults.Web);
var bien = JsonSerializer.Deserialize<Ficha>(json, web)!;
Console.WriteLine($"Con las opciones web: '{bien.Titulo}', {bien.Anio}");
class Ficha
{
public string? Titulo { get; set; }
public string? Autor { get; set; }
public int Anio { get; set; }
}salidaSin opciones: '', 0 Con las opciones web: 'Rayuela', 1963
Deserialize sin opciones busca exactamente Titulo y no encuentra titulo: no da ningún error, simplemente deja las propiedades vacías. Es el fallo más común con System.Text.Json. Usa las opciones Web (o PropertyNameCaseInsensitive = true) y crea el objeto de opciones una vez: guardan en caché la información de cada tipo, y crearlas en cada llamada es lento.Nombres, nulos y enums a medida
#:property PublishAot=false
using System.Text.Json;
using System.Text.Json.Serialization;
var pedido = new Pedido(1042, Estado.Enviado, null, 59.90m);
var opciones = new JsonSerializerOptions
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() },
};
Console.WriteLine(JsonSerializer.Serialize(pedido, opciones));
record Pedido(
[property: JsonPropertyName("id_pedido")] int Id,
Estado Estado,
string? Nota,
decimal Total);
enum Estado { Pendiente, Enviado, Entregado }salida{
"id_pedido": 1042,
"Estado": "Enviado",
"Total": 59.90
}Con [JsonPropertyName] una propiedad se llama en el JSON como diga la API, aunque en C# siga las normas de C#. En un record posicional el atributo lleva property: delante, para que se aplique a la propiedad y no al parámetro. WhenWritingNull omite la nota, que es null, y JsonStringEnumConverter escribe el enum como texto en vez de como número (1).
JSON sin clases: JsonNode
Para leer un par de valores de un JSON grande, o modificarlo, no hace falta declarar tipos. JsonNode lo recorre como un árbol:
#:property PublishAot=false
using System.Text.Json.Nodes;
var raiz = JsonNode.Parse("""
{"tienda": "Centro", "ventas": [{"producto": "café", "uds": 3}, {"producto": "té", "uds": 5}]}
""")!;
Console.WriteLine(raiz["tienda"]);
foreach (var v in raiz["ventas"]!.AsArray())
{
Console.WriteLine($"{v!["producto"]}: {(int)v["uds"]!}");
}
raiz["ventas"]!.AsArray().Add(new JsonObject { ["producto"] = "cacao", ["uds"] = 2 });
raiz["abierta"] = true;
Console.WriteLine(raiz.ToJsonString());salidaCentro
café: 3
té: 5
{"tienda":"Centro","ventas":[{"producto":"caf\u00E9","uds":3},{"producto":"t\u00E9","uds":5},{"producto":"cacao","uds":2}],"abierta":true}JSON mal formado
#:property PublishAot=false
using System.Text.Json;
try
{
var x = JsonSerializer.Deserialize<Dictionary<string, int>>("""{"uno": 1, "dos": "2"}""");
}
catch (JsonException ex)
{
Console.WriteLine(ex.Message);
}salidaThe JSON value could not be converted to System.Int32. Path: $.dos | LineNumber: 0 | BytePositionInLine: 21.
Un JSON roto, o con un valor del tipo equivocado (aquí, el texto "2" donde se esperaba un número), lanza JsonException. El mensaje dice en qué ruta y en qué posición está el problema: $.dos es la propiedad dos de la raíz.
Leer y escribir archivos
#:property PublishAot=false
using System.Text.Json;
var agenda = new List<Contacto> { new("Ana", "600111222"), new("Luis", "600333444") };
await using (var salida = File.Create("agenda.json"))
{
await JsonSerializer.SerializeAsync(salida, agenda);
}
await using var entrada = File.OpenRead("agenda.json");
var leida = await JsonSerializer.DeserializeAsync<List<Contacto>>(entrada);
Console.WriteLine($"{leida!.Count} contactos; el primero es {leida[0]}");
record Contacto(string Nombre, string Telefono);salida2 contactos; el primero es Contacto { Nombre = Ana, Telefono = 600111222 }SerializeAsync y DeserializeAsync trabajan directamente sobre el archivo, sin cargar el texto entero en memoria. Para algo pequeño también vale File.WriteAllText("agenda.json", JsonSerializer.Serialize(agenda)).
Sin reflexión: el contexto generado
El otro camino para los archivos sueltos, y el obligatorio si publicas con AOT, es que el compilador escriba el código de conversión. Se declara una clase partial que hereda de JsonSerializerContext con un atributo por cada tipo, y se le pasa a Serialize y Deserialize. Este programa no lleva PublishAot=false:
using System.Text.Json;
using System.Text.Json.Serialization;
var libro = new Libro("Pedro Páramo", "Juan Rulfo", 1955);
string json = JsonSerializer.Serialize(libro, Contexto.Default.Libro);
Console.WriteLine(json);
var vuelta = JsonSerializer.Deserialize(json, Contexto.Default.Libro);
Console.WriteLine(vuelta);
record Libro(string Titulo, string Autor, int Anio);
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
[JsonSerializable(typeof(Libro))]
partial class Contexto : JsonSerializerContext;salida{"titulo":"Pedro P\u00E1ramo","autor":"Juan Rulfo","anio":1955}
Libro { Titulo = Pedro Páramo, Autor = Juan Rulfo, Anio = 1955 }Además de funcionar con AOT, es más rápido al arrancar, porque no hay que inspeccionar los tipos al ejecutar. Las opciones van en el atributo JsonSourceGenerationOptions; aquí, las web, por eso los nombres salen en camelCase.
Todo el código de esta página se compiló con el .NET SDK 10.0.403 (C# 14) y se ejecutó con .NET 10.0.14 en un contenedor limpio antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.