1. Introduction
For a long time in the .NET ecosystem the main tool for working with JSON was the excellent third-party package Newtonsoft.Json (aka Json.NET). It's powerful, flexible, and still widely used. But with the arrival of newer .NET 9 and C# 14 versions, Microsoft decided it's time to have its own built-in, high-performance JSON serializer. That's how System.Text.Json came to be.
Why a new approach? System.Text.Json was designed with modern requirements in mind and addresses issues that accumulated over years of using third-party libraries. It's optimized for maximum speed and security, fits perfectly for async scenarios and web APIs, and — best of all — doesn't require installation via NuGet: it's already in the platform.
Of course, Newtonsoft.Json didn't disappear, and we'll look at it later. But for most new projects System.Text.Json is the default choice. Get ready — we're going to teach our objects to speak JSON!
2. Basics of working with System.Text.Json
Simple object serialization
Alright, let's start with the basic magic. We'll serialize our object to a JSON string.
using System;
using System.Text.Json; // Don't forget to add!
public class Player
{
public string Name { get; set; }
public int Health { get; set; }
public bool IsAlive { get; set; }
}
// Somewhere in your program:
Player player = new Player { Name = "Aragorn", Health = 100, IsAlive = true };
// Serialization:
string json = JsonSerializer.Serialize(player);
Console.WriteLine(json); // Will print: {"Name":"Aragorn","Health":100,"IsAlive":true}
Note: If you're just starting with serialization, this example shows how simple it is: JsonSerializer.Serialize — and you're done.
Object deserialization
Let's bring an object back to life from a JSON string:
string incomingJson = "{\"Name\":\"Legolas\",\"Health\":88,\"IsAlive\":true}";
Player player2 = JsonSerializer.Deserialize<Player>(incomingJson);
Console.WriteLine(player2.Name); // Legolas
Console.WriteLine(player2.Health); // 88
Console.WriteLine(player2.IsAlive); // true
Note: If the JSON structure matches your class — everything works like clockwork. If not — you might get exceptions or default values.
3. Principles and internals of serialization
How matching works
System.Text.Json by default uses the same property names as in your class. Case matters! If the JSON has health instead of Health, deserialization won't work — the property will remain at its default value (0, false, or null).
For example:
// JSON with lowercase keys:
string badJson = "{\"name\":\"Gimli\",\"health\":120,\"isAlive\":true}";
Player player3 = JsonSerializer.Deserialize<Player>(badJson);
Console.WriteLine(player3.Name); // empty
Console.WriteLine(player3.Health); // 0
Console.WriteLine(player3.IsAlive); // false
Fun fact: Many APIs use camelCase keys (health), while in C# it's customary to use PascalCase (Health). This can be fixed with settings (below).
4. Controlling serialization — options and settings
JSON formatting: pretty-printed output
Sometimes you want not a compact but a nicely formatted JSON — for configs or logs.
var options = new JsonSerializerOptions
{
WriteIndented = true // Add indentation
};
string prettyJson = JsonSerializer.Serialize(player, options);
Console.WriteLine(prettyJson);
/*
{
"Name": "Frodo",
"Health": 50,
"IsAlive": true,
"Inventory": [
"Ring",
"Bread",
"Torch"
],
"Position": {
"X": 5,
"Y": 15
}
}
*/
The WriteIndented property is used, and the serializer adds indentation and line breaks.
Controlling naming style (CamelCase vs PascalCase)
If you're working with a web API where all keys are in camelCase, enable the naming policy:
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string camelCaseJson = JsonSerializer.Serialize(player, options);
// {"name":"Frodo","health":50,"isAlive":true,"inventory":["Ring","Bread","Torch"],"position":{"x":5,"y":15}}
Then deserialization will correctly match such keys as well:
string apiJson = "{\"name\":\"Bilbo\",\"health\":40,\"isAlive\":true,\"inventory\":[\"Mug\"],\"position\":{\"x\":10,\"y\":5}}";
Player bilbo = JsonSerializer.Deserialize<Player>(apiJson, options);
Console.WriteLine(bilbo.Name); // Bilbo
5. Useful nuances
Using the [JsonIgnore] attribute
Sometimes not all properties should be serialized — for example, private data or temporary computed values.
using System.Text.Json.Serialization;
public class Player
{
public string Name { get; set; }
public int Health { get; set; }
[JsonIgnore] // This property won't get into JSON
public bool IsSecretCharacter { get; set; }
}
Now when serializing:
var player = new Player { Name = "Boromir", Health = 80, IsSecretCharacter = true };
string json = JsonSerializer.Serialize(player);
Console.WriteLine(json); // {"Name":"Boromir","Health":80}
On deserialization IsSecretCharacter will get the default value (false).
Using [JsonPropertyName("...")]
Say in code the property is called IsAlive, but in JSON it should be "status":
using System.Text.Json.Serialization;
public class Player
{
public string Name { get; set; }
public int Health { get; set; }
[JsonPropertyName("status")]
public bool IsAlive { get; set; }
}
Serialization will now look like:
var player = new Player { Name = "Pippin", Health = 60, IsAlive = false };
string json = JsonSerializer.Serialize(player);
Console.WriteLine(json); // {"Name":"Pippin","Health":60,"status":false}
And during deserialization the "status" key will correctly fill the IsAlive property.
Built-in limitations and security specifics
- By default only public properties with getters/setters are serialized; private fields/properties are ignored.
- For cyclical references an exception is thrown: "A possible object cycle was detected".
- Unlike Newtonsoft.Json, the standard serializer relies less on "magic" type tricks — which makes it safer and faster for common scenarios.
6. Common mistakes and pitfalls
You accidentally change a property's JSON name and forget to update the code — as a result the property gets a default value (null, 0, false).
The needed field is missing in JSON — the corresponding object property will be default (see the documentation).
The property has no public setter — it won't be populated during deserialization.
You changed the structure of nested classes or collections — deserialization can break or produce unexpected results.
Identical names at different nesting levels (in a parent and child object) can be confusing and complicate debugging.
Sometimes the reason is the platform version: older versions of System.Text.Json handled some types worse (for example, Dictionary, DateTime, enum), but in .NET 7/8/9 many issues have been fixed.
GO TO FULL VERSION