The JSON format (JavaScript Object Notation) is a lightweight text format for representing structured data.
JSON has taken over the world. It’s lightweight, human-readable, and is the de facto standard for REST APIs, configuration files (appsettings.json), and NoSQL databases.
In the .NET ecosystem, for years we relied on a wonderful external library called Newtonsoft.Json (Json.NET). However, since .NET Core 3.0, Microsoft rewrote everything from scratch to create System.Text.Json.
This new library is native, secure by default, and obsessively optimized for performance, using low-level structures like Span<T> and UTF8 directly in memory.
Basic Serialization
The static class we’ll use for almost everything is JsonSerializer.
Let’s start with a simple model class. Remember that for the serializer to include properties by default, they must be public.
public class Videojuego
{
public string Titulo { get; set; }
public int Anio { get; set; }
public string[] Plataformas { get; set; }
}To convert an object into JSON text:
using System.Text.Json;
var juego = new Videojuego
{
Titulo = "Hollow Knight",
Anio = 2017,
Plataformas = new[] { "PC", "Switch", "PS4" }
};
string json = JsonSerializer.Serialize(juego);
Console.WriteLine(json);
// Output: {"Titulo":"Hollow Knight","Anio":2017,"Plataformas":["PC","Switch","PS4"]}By default, System.Text.Json minifies the output (removes whitespace) to save bytes during network transmission.
Deserialization and Parsing
For the reverse process, we use Deserialize<T>. The serializer will try to map JSON keys to the properties of class T.
string jsonEntrada = @"{ ""Titulo"": ""Celeste"", ""Anio"": 2018 }";
// Convert the JSON string into a C# object
Videojuego juegoRecuperado = JsonSerializer.Deserialize<Videojuego>(jsonEntrada);
Console.WriteLine(juegoRecuperado.Titulo); // CelesteBy default, deserialization is Case Sensitive. If the JSON has "titulo": "Celeste" (lowercase) and your class has Titulo (uppercase), it won’t map it and will leave the property as null. We’ll see how to fix this shortly.
Configuring the Serializer (JsonSerializerOptions)
The default behavior is strict and fast. But the real world is chaotic. To adapt, we use JsonSerializerOptions.
Human-Readable JSON and Case-Insensitive Names
If we want to generate readable configuration files or read JSONs from JavaScript APIs (which use camelCase), we need this:
var opciones = new JsonSerializerOptions
{
WriteIndented = true, // Adds spaces and line breaks
PropertyNameCaseInsensitive = true // Ignores case when reading
};
string jsonBonito = JsonSerializer.Serialize(juego, opciones);
/* Output:
{
"Titulo": "Hollow Knight",
"Anio": 2017,
...
}
*/Naming Policies
In C#, we use PascalCase (MiPropiedad), but in web JSON, camelCase (miPropiedad) is standard. We can automate this translation:
var opcionesWeb = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
// Now 'Titulo' will be automatically serialized as 'titulo'Control Attributes
Sometimes global configuration isn’t enough. An external API might send an ugly property name you don’t want to use in your C# code, or you might want to hide data.
For this, we decorate our class properties with attributes from System.Text.Json.Serialization.
[JsonPropertyName("ugly_name")]: Maps a C# property to a specific JSON key.[JsonIgnore]: Omits the property. It is neither sent nor read.[JsonInclude]: Allows including members the serializer wouldn’t take by default, such as public properties with a non-public setter.
using System.Text.Json.Serialization;
public class Usuario
{
[JsonPropertyName("user_id")] // In JSON, it will be "user_id"
public int Id { get; set; }
public string Nombre { get; set; }
[JsonIgnore] // Will never leave here
public string PasswordHash { get; set; }
}Asynchronous Serialization with Streams
If you need to save a large object to a file or send it over the network, don’t convert it to a string first.
- Object to String (uses more RAM).
- String to File.
It’s much more efficient to write directly from the object to the Stream, without an intermediate string.
using FileStream createStream = File.Create("data.json");
// Writes directly to disk byte by byte
await JsonSerializer.SerializeAsync(createStream, juego);And for reading:
using FileStream openStream = File.OpenRead("data.json");
// Reads from disk and builds the object on the fly
Videojuego juego = await JsonSerializer.DeserializeAsync<Videojuego>(openStream);This drastically reduces memory usage (Garbage Collection) in high-performance applications.
Working with Dynamic JSON (JsonNode)
C# is statically typed, but sometimes we receive a JSON whose structure we don’t know, or we just want to read a property without creating an entire class for it.
Since .NET 6, we have the JSON DOM API with JsonNode. It’s similar to working with a Dictionary.
string json = "{\"temperatura\": 25, \"detalles\": { \"sensor\": \"A1\" } }";
// Parse it into a generic node
JsonNode nodo = JsonNode.Parse(json);
// Navigate as if it were an associative array
int temp = (int)nodo["temperatura"];
string sensor = (string)nodo["detalles"]["sensor"];
Console.WriteLine($"Sensor {sensor} reads {temp}º");