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 pongasIncludeFields = trueen las opciones o conviertas el campo en una propiedad. - Los enums son números.
OrderStatus.Shippedse escribe como1(el valor subyacente del enum). AñadeJsonStringEnumConverter(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.Json | Newtonsoft.Json | |
|---|---|---|
| Disponibilidad | incorporado en .NET Core 3.0+; paquete NuGet para .NET Framework 4.6.2+ | paquete NuGet para .NET Framework y .NET |
| Serializar / deserializar | JsonSerializer.Serialize(obj) / Deserialize<T>(json) | JsonConvert.SerializeObject(obj) / DeserializeObject<T>(json) |
| Coincidencia de nombres | distingue mayúsculas por defecto | no distingue mayúsculas |
| Lectura sin tipos | JsonDocument, JsonNode | JObject, JToken, con consultas JSONPath |
| Renombrar una propiedad | [JsonPropertyName("x")] | [JsonProperty("x")] |
| Permisividad | estricto: sin comentarios, comas finales ni números entre comillas salvo que se activen | permisivo por defecto |
| Rendimiento | más rápido, menos reservas de memoria, generación de código para trimming y AOT | má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
PropertyNameCaseInsensitiveoJsonSerializerDefaults.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
JsonSerializerOptionsnuevo en cada llamada. Reutiliza una instancia static. - Dinero en coma flotante. Un
doublecon0.1 + 0.2se serializa como0.30000000000000004. Usadecimalpara 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.