C# JSON Serialization
System.Text.Json Overview
System.Text.Json is the built-in, high-performance JSON library in .NET. Itβs the default for ASP.NET Core and recommended for most scenarios.
using System.Text.Json;
// Serialize
var person = new Person { Name = "Alice", Age = 30 };
string json = JsonSerializer.Serialize(person);
// {"Name":"Alice","Age":30}
// Deserialize
Person? restored = JsonSerializer.Deserialize<Person>(json);
Serialization Options
var options = new JsonSerializerOptions
{
// Naming
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // name, age
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
// Formatting
WriteIndented = true,
// Null handling
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
// Property matching
PropertyNameCaseInsensitive = true,
// Number handling
NumberHandling = JsonNumberHandling.AllowReadingFromString,
// Encoder (allow non-ASCII characters)
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
string json = JsonSerializer.Serialize(person, options);
Reuse Options
// Options are cached internally - reuse for performance
private static readonly JsonSerializerOptions SharedOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true
};
public string ToJson<T>(T obj) => JsonSerializer.Serialize(obj, SharedOptions);
Attributes
public class Product
{
[JsonPropertyName("product_id")]
public int Id { get; set; }
[JsonIgnore]
public string InternalCode { get; set; } = "";
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Description { get; set; }
[JsonInclude]
private string _secretField = ""; // Include private member
[JsonPropertyOrder(1)] // Control order
public string Name { get; set; } = "";
[JsonConverter(typeof(JsonStringEnumConverter))]
public ProductStatus Status { get; set; }
[JsonNumberHandling(JsonNumberHandling.WriteAsString)]
public decimal Price { get; set; }
}
public enum ProductStatus { Active, Discontinued }
Common Scenarios
Working with Streams
// Async serialization to stream
await using var stream = File.Create("data.json");
await JsonSerializer.SerializeAsync(stream, data, options);
// Async deserialization from stream
await using var readStream = File.OpenRead("data.json");
var result = await JsonSerializer.DeserializeAsync<List<Product>>(readStream);
// From HttpClient
var response = await httpClient.GetAsync(url);
var data = await response.Content.ReadFromJsonAsync<Product>();
Choosing How to Work with JSON
JsonDocument (Read-Only)
- Pooled and highly efficient
- Extract specific values
- No object allocation for full tree
- Disposed after use
JsonNode (Mutable)
- In-memory representation
- Modify JSON dynamically
- Build JSON programmatically
- Stays in memory
System.Text.Json offers multiple approaches depending on your needs.
| Approach | Use When |
|---|---|
JsonSerializer.Deserialize<T> |
You have a known type that matches the JSON structure |
JsonDocument |
Read-only access, extracting specific values, validation |
JsonNode |
Need to modify JSON or build JSON dynamically |
| Source generation | Production code needing performance and AOT support |
JsonDocument is a read-only, pooled, and highly efficient way to navigate JSON. Use it when you only need to read values and donβt want to allocate objects for the entire structure.
JsonNode creates a mutable in-memory representation. Use it when you need to modify JSON (add/remove properties, change values) or build JSON programmatically.
Typed deserialization is best when your JSON structure is known and stable. It provides compile-time safety and IntelliSense.
Dynamic JSON with JsonDocument
// Parse without deserializing to specific type
using JsonDocument doc = JsonDocument.Parse(json);
JsonElement root = doc.RootElement;
// Navigate properties
string name = root.GetProperty("name").GetString()!;
int age = root.GetProperty("age").GetInt32();
// Check if property exists
if (root.TryGetProperty("email", out JsonElement email))
{
Console.WriteLine(email.GetString());
}
// Iterate arrays
foreach (JsonElement item in root.GetProperty("items").EnumerateArray())
{
Console.WriteLine(item.GetProperty("id").GetInt32());
}
// Iterate object properties
foreach (JsonProperty prop in root.EnumerateObject())
{
Console.WriteLine($"{prop.Name}: {prop.Value}");
}
JsonNode for Modification
using System.Text.Json.Nodes;
// Parse to mutable structure
JsonNode? node = JsonNode.Parse(json);
// Read values
string? name = node?["name"]?.GetValue<string>();
// Modify
node!["name"] = "Bob";
node["newProperty"] = 42;
// Build from scratch
var obj = new JsonObject
{
["name"] = "Alice",
["age"] = 30,
["tags"] = new JsonArray("developer", "reader")
};
string result = obj.ToJsonString();
Custom Converters
// Custom DateTime format
public class DateOnlyConverter : JsonConverter<DateOnly>
{
private const string Format = "yyyy-MM-dd";
public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert,
JsonSerializerOptions options)
{
return DateOnly.ParseExact(reader.GetString()!, Format);
}
public override void Write(Utf8JsonWriter writer, DateOnly value,
JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString(Format));
}
}
// Register converter
var options = new JsonSerializerOptions
{
Converters = { new DateOnlyConverter() }
};
// Or use attribute
public class Event
{
[JsonConverter(typeof(DateOnlyConverter))]
public DateOnly Date { get; set; }
}
Polymorphic Serialization
[JsonDerivedType(typeof(Student), "student")]
[JsonDerivedType(typeof(Teacher), "teacher")]
public class Person
{
public string Name { get; set; } = "";
}
public class Student : Person
{
public string Major { get; set; } = "";
}
public class Teacher : Person
{
public string Subject { get; set; } = "";
}
// Serializes with $type discriminator
// {"$type":"student","Name":"Alice","Major":"CS"}
Source Generation
Source Generation for Production
Eliminates reflection overhead, enables AOT compilation, and makes trimming work correctly. Use it for production applications where startup time and deployment size matter.
Compile-time generated serialization for better performance and AOT support.
[JsonSerializable(typeof(Person))]
[JsonSerializable(typeof(List<Person>))]
[JsonSourceGenerationOptions(
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
WriteIndented = true)]
public partial class AppJsonContext : JsonSerializerContext { }
// Usage - no reflection at runtime
string json = JsonSerializer.Serialize(person, AppJsonContext.Default.Person);
Person? p = JsonSerializer.Deserialize(json, AppJsonContext.Default.Person);
// With HttpClient
var response = await httpClient.GetFromJsonAsync(url,
AppJsonContext.Default.Person);
Error Handling
try
{
var result = JsonSerializer.Deserialize<Product>(json);
}
catch (JsonException ex)
{
Console.WriteLine($"JSON error at {ex.Path}: {ex.Message}");
Console.WriteLine($"Line: {ex.LineNumber}, Position: {ex.BytePositionInLine}");
}
Version History
| Feature | Version | Significance |
|---|---|---|
| System.Text.Json | .NET Core 3.0 | Built-in JSON support |
| JsonDocument | .NET Core 3.0 | Read-only DOM |
| JsonNode | .NET 6 | Mutable DOM |
| Source generation | .NET 6 | Compile-time serialization |
| JsonDerivedType | .NET 7 | Polymorphic serialization |
| Required properties | .NET 7 | required keyword support |
Key Takeaways
Use source generation for production: Eliminates reflection overhead and enables AOT compilation.
Reuse JsonSerializerOptions: Create once and reuse to benefit from internal caching.
JsonDocument for read-only access: Efficient for extracting values without full deserialization.
JsonNode for modifications: When you need to read, modify, and write JSON dynamically.
Configure naming policy: CamelCase is standard for web APIs.
Found this guide helpful? Share it with your team:
Share on LinkedIn