Menu

JSON en C#: serializar y deserializar con System.Text.Json

Cómo trabajar con JSON en C# usando System.Text.Json: JsonSerializer.Serialize y Deserialize, salida en camelCase e indentada, atributos como JsonPropertyName y JsonIgnore, enums como strings, leer JSON sin clases con JsonDocument y JsonNode, los errores y la comparación con Newtonsoft.Json.

JSON es la forma en que la mayoría de los programas C# hablan con las API web, guardan la configuración e intercambian datos. El .NET moderno lo lee y lo escribe con System.Text.Json, que forma parte del runtime desde .NET Core 3.0: no hace falta ningún paquete NuGet. Su punto de entrada principal es la clase static JsonSerializer, que convierte objetos en strings JSON y viceversa.

System.Text.Json viene incorporado en .NET Core 3.0 y en todas las versiones posteriores (de .NET 5 a .NET 10). Los proyectos en .NET Framework 4.6.2 o posterior, o en .NET Standard 2.0, también pueden usarlo instalando el paquete NuGet System.Text.Json. Los ejemplos de esta página se muestran como código normal con la salida en comentarios. Añade estas directivas using para ejecutarlos en un proyecto .NET:

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

Serializar: de objeto a JSON

JsonSerializer.Serialize escribe todas las propiedades públicas de un objeto, usando los nombres de las propiedades tal como son:

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}

Las colecciones se convierten en arrays, los diccionarios con claves string en objetos ({"apples":3,"pears":5}), null sigue siendo null y DateTime se convierte en un string ISO 8601 ("2026-03-01T14:30:00"). Dos valores por defecto pillan a la gente:

  • Los campos se omiten. Solo se serializan las propiedades. Una clase con public int X; se serializa como {} salvo que pongas IncludeFields = true en las opciones o conviertas el campo en una propiedad.
  • Los enums son números. OrderStatus.Shipped se escribe como 1 (el valor subyacente del enum). Añade JsonStringEnumConverter (más abajo) para escribir "Shipped".

Deserializar: de JSON a objeto

JsonSerializer.Deserialize<T> crea un T y asigna sus propiedades a partir del 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

Las propiedades JSON sin propiedad C# correspondiente se ignoran, y las propiedades C# sin JSON correspondiente conservan sus valores por defecto. Ninguna de las dos cosas es un error por defecto.

La regla que pilla a casi todo el mundo: la coincidencia de nombres distingue mayúsculas y minúsculas. La mayoría de las API web envían camelCase, y camelCase no coincide con las propiedades en 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) da la configuración que usa ASP.NET Core: lectura sin distinguir mayúsculas, escritura en camelCase y números aceptados como strings entre comillas.

La deserialización necesita una forma de asignar cada valor: un setter público, un accesor init o un constructor cuyos nombres de parámetros coincidan con las propiedades. Esa última regla es la razón por la que los records funcionan sin más:

public record Point(int X, int Y);

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

Opciones: camelCase y salida indentada

JsonSerializerOptions controla los nombres, el formato y más cosas. Crea una instancia y reutilízala: el serializador guarda metadatos en caché por cada instancia de opciones, así que crear opciones nuevas en cada llamada es apreciablemente más 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
// }

Otras opciones que conviene conocer: DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull para omitir las propiedades null, IncludeFields = true, NumberHandling para aceptar números escritos como strings, y ReadCommentHandling = JsonCommentHandling.Skip más AllowTrailingCommas = true para archivos de configuración editados a mano. .NET 8 añadió JsonNamingPolicy.SnakeCaseLower para las API que usan snake_case.

Atributos: renombrar, ignorar, enums como strings

Los atributos en la clase controlan una propiedad cada vez y tienen prioridad sobre las opciones:

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 escribir todos los enums como strings en lugar de marcar cada propiedad, añade el convertidor a las opciones: options.Converters.Add(new JsonStringEnumConverter());.

Leer JSON sin una clase: JsonDocument y JsonNode

Cuando solo necesitas unos pocos valores de una respuesta grande, o su forma varía, prescinde de la clase. JsonDocument parsea a un árbol de solo lectura 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 lanza KeyNotFoundException si falta el nombre, así que usa TryGetProperty para los campos opcionales. JsonDocument toma prestada memoria de un pool, y por eso se libera con using.

Para modificar JSON, usa JsonNode de System.Text.Json.Nodes (.NET 6 y posteriores), que da un árbol mutable con 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}

Archivos y streams

El JSON en disco es un string en un archivo, así que los métodos de archivos se combinan directamente con el 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 archivos grandes y cuerpos HTTP, las sobrecargas async con streams evitan construir el string entero en memoria:

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

En ASP.NET Core y con HttpClient, rara vez llamas tú al serializador: los controladores enlazan automáticamente los cuerpos JSON, y httpClient.GetFromJsonAsync<Order>(url) (en System.Net.Http.Json) hace la petición y la deserialización en una sola llamada.

Errores

Un JSON no válido, o un valor que no puede convertirse al tipo de la propiedad, lanza JsonException. Su mensaje indica la ruta JSON y la posición, lo que suele bastar para encontrar el 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 devuelve null (no una excepción) cuando el texto JSON es el literal null, así que comprueba el resultado cuando la entrada venga de fuera.

Caracteres escapados en la salida

Por defecto, el serializador escapa los caracteres no ASCII y los que no son seguros dentro de HTML:

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

Es JSON válido y se vuelve a leer como el texto original; se escapa para que la salida pueda meterse en una página HTML sin riesgo. Para archivos legibles por personas, pon Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (de System.Text.Encodings.Web) en las opciones. Lo de "unsafe" se refiere solo a incrustar el resultado en HTML.

System.Text.Json frente a Newtonsoft.Json

Newtonsoft.Json (Json.NET, la clase JsonConvert) fue el estándar durante una década y sigue estando en todas partes. Las principales diferencias:

System.Text.JsonNewtonsoft.Json
Disponibilidadincorporado en .NET Core 3.0+; paquete NuGet para .NET Framework 4.6.2+paquete NuGet para .NET Framework y .NET
Serializar / deserializarJsonSerializer.Serialize(obj) / Deserialize<T>(json)JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json)
Coincidencia de nombresdistingue mayúsculas por defectono distingue mayúsculas
Lectura sin tiposJsonDocument, JsonNodeJObject, JToken, con consultas JSONPath
Renombrar una propiedad[JsonPropertyName("x")][JsonProperty("x")]
Permisividadestricto: sin comentarios, comas finales ni números entre comillas salvo que se activenpermisivo por defecto
Rendimientomás rápido, menos reservas de memoria, generación de código para trimming y AOTmás lento, basado en reflexión

Los atributos tienen nombres y namespaces distintos, así que migrar una base de código es un trabajo de buscar y sustituir más pruebas para el parseo más estricto. Para el código .NET nuevo, empieza con System.Text.Json.

Generación de código fuente (.NET 6+)

JsonSerializer normalmente inspecciona tus tipos con reflexión en tiempo de ejecución. Para las aplicaciones recortadas o compiladas de antemano (Native AOT, Blazor WebAssembly), un generador de código fuente escribe ese código en tiempo de compilación:

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

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

Errores comunes

  • JSON en camelCase hacia propiedades en PascalCase con las opciones por defecto. Todas las propiedades se quedan vacías, en silencio. Usa PropertyNameCaseInsensitive o JsonSerializerDefaults.Web.
  • Campos públicos en lugar de propiedades. No se serializan salvo que se active IncludeFields.
  • Propiedades sin setter. Una propiedad de solo lectura sin un parámetro de constructor que le corresponda no se rellena al deserializar.
  • Un JsonSerializerOptions nuevo en cada llamada. Reutiliza una instancia static.
  • Dinero en coma flotante. Un double con 0.1 + 0.2 se serializa como 0.30000000000000004. Usa decimal para las cantidades.

Preguntas frecuentes

¿Cómo convierto un objeto a JSON en C#?

Llama a JsonSerializer.Serialize(obj) de System.Text.Json, que viene incorporado en .NET Core 3.0 y posteriores sin ningún paquete que instalar. Escribe todas las propiedades públicas: {"Name":"Desk lamp","Price":34.90}. Pasa new JsonSerializerOptions { WriteIndented = true } para una salida legible y PropertyNamingPolicy = JsonNamingPolicy.CamelCase para nombres en camelCase.

¿Cómo convierto JSON a un objeto en C#?

var product = JsonSerializer.Deserialize<Product>(json); crea un Product y rellena sus propiedades públicas asignables a partir de los nombres JSON que coinciden. La coincidencia distingue mayúsculas por defecto, así que un JSON en camelCase deja vacías las propiedades en PascalCase salvo que pases PropertyNameCaseInsensitive = true o new JsonSerializerOptions(JsonSerializerDefaults.Web). Un JSON mal formado o un valor del tipo equivocado lanza JsonException.

¿Cómo leo JSON sin crear una clase en C#?

Usa JsonDocument.Parse(json) y recorre RootElement con GetProperty("name"), GetString(), GetInt32() y EnumerateArray(); es de solo lectura y rápido, y hay que liberarlo. Para un JSON que quieras modificar, JsonNode.Parse(json) (.NET 6+) da un árbol mutable: node["city"], asignaciones y ToJsonString().

¿Debo usar System.Text.Json o Newtonsoft.Json?

Para código nuevo en .NET Core 3.0 o posterior, System.Text.Json: viene incorporado, es más rápido, reserva menos memoria y ASP.NET Core lo usa por defecto. Newtonsoft.Json (Json.NET) sigue siendo habitual en los proyectos de .NET Framework (donde System.Text.Json solo está disponible como paquete NuGet), en el código que depende de sus funciones extra (consultas JSONPath, un parseo muy permisivo, TypeNameHandling) y en las bases de código grandes ya construidas sobre él.

¿Por qué System.Text.Json escapa caracteres como é y <?

El codificador por defecto escapa los caracteres no ASCII y los sensibles en HTML (<, >, &, ') como \uXXXX, para que la salida pueda incrustarse en HTML sin riesgo. Sigue siendo JSON válido y se deserializa de vuelta al mismo texto. Para una salida legible, pon Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping en las opciones, pero solo cuando el JSON no vaya a escribirse dentro de HTML.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR