📝 Lesson 4.2: JSON Serialization
Text files are fine for plain strings, but real apps store structured data — objects with properties, lists of records. JSON is the universal format for that, and C# converts your objects to and from it in a single line.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain what serialization and JSON are
- Serialize objects and collections with
JsonSerializer.Serialize - Deserialize JSON back into objects with
JsonSerializer.Deserialize - Control output with
JsonSerializerOptions - Save and load objects to a file by combining JSON with file I/O
Estimated Time: 60 minutes
Project: Save a list of objects to a JSON file and load them back.
In This Lesson
What Is Serialization?
Serialization is converting an object in memory into a format that can be stored or transmitted — usually text. Deserialization is the reverse: rebuilding the object from that text. JSON (JavaScript Object Notation) is the most common format for this.
📖 Definition
JSON: a lightweight, human-readable text format for structured data, using key/value pairs and lists. It's the lingua franca of web APIs, config files, and saved app state.
A C# object and its JSON look like mirror images of each other:
Product { Name, Price }"] -->|"Serialize"| B["JSON text
{ 'name': 'Keyboard', 'price': 45 }"] B -->|"Deserialize"| A style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style B fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
.NET's built-in JSON tools live in System.Text.Json. (You may need using System.Text.Json; at the top.)
Serializing to JSON
JsonSerializer.Serialize turns any object into a JSON string. It automatically includes the object's public properties:
using System.Text.Json;
record Product(string Name, string Category, decimal Price);
Product keyboard = new Product("Keyboard", "Electronics", 45m);
string json = JsonSerializer.Serialize(keyboard);
Console.WriteLine(json);
// {"Name":"Keyboard","Category":"Electronics","Price":45}
Collections serialize just as easily — a List becomes a JSON array:
List<Product> products = new List<Product>
{
new Product("Keyboard", "Electronics", 45m),
new Product("Desk", "Furniture", 150m)
};
string json = JsonSerializer.Serialize(products);
Console.WriteLine(json);
// [{"Name":"Keyboard",...},{"Name":"Desk",...}]
💡 Works with records and classes
The record types from Lesson 3.1 serialize perfectly, and so do regular classes. Serialization reads your object's public properties — no extra setup required for typical data types.
Deserializing from JSON
JsonSerializer.Deserialize<T> rebuilds an object from JSON text. You tell it the target type with a generic argument (Lesson 1.2), and it fills in the properties by matching names:
string json = "{\"Name\":\"Mouse\",\"Category\":\"Electronics\",\"Price\":25}";
Product? product = JsonSerializer.Deserialize<Product>(json);
Console.WriteLine(product?.Name); // Mouse
Console.WriteLine(product?.Price); // 25
Deserializing a JSON array back into a List:
string json = "[{\"Name\":\"Keyboard\",\"Category\":\"Electronics\",\"Price\":45}]";
List<Product>? items = JsonSerializer.Deserialize<List<Product>>(json);
Console.WriteLine(items?.Count); // 1
⚠️ Deserialize can return null / throw
Deserialize<T> returns a nullable result (note the ? — Lesson 3.3), and malformed JSON throws a JsonException. For untrusted input, check for null and wrap the call in try/catch (JsonException) — the exception habits from Lesson 1.1 apply here too.
Options: Formatting & Naming
By default the JSON is compact and uses your C# property names (PascalCase). Pass a JsonSerializerOptions to change that — two options you'll use constantly:
var options = new JsonSerializerOptions
{
WriteIndented = true, // pretty-print
PropertyNamingPolicy = JsonNamingPolicy.CamelCase // camelCase keys
};
string json = JsonSerializer.Serialize(keyboard, options);
Console.WriteLine(json);
Output:
{
"name": "Keyboard",
"category": "Electronics",
"price": 45
}
💡 Naming matters for interop
WriteIndented makes JSON readable for humans and config files. CamelCase naming matches the convention most web APIs and JavaScript expect — important when your JSON talks to other systems (as it will in Lesson 5.1). Use the same options for serializing and deserializing so names line up.
Saving Objects to a File
Now combine this lesson with the last one: serialize to a JSON string, then write it to a file with the File class. This is a complete "save and load" for structured data.
using System.Text.Json;
var options = new JsonSerializerOptions { WriteIndented = true };
string path = "products.json";
// SAVE: object → JSON → file
List<Product> products = new List<Product>
{
new Product("Keyboard", "Electronics", 45m),
new Product("Desk", "Furniture", 150m)
};
string json = JsonSerializer.Serialize(products, options);
File.WriteAllText(path, json);
Console.WriteLine("Saved!");
// LOAD: file → JSON → object
if (File.Exists(path))
{
string loaded = File.ReadAllText(path);
List<Product>? restored = JsonSerializer.Deserialize<List<Product>>(loaded);
Console.WriteLine($"Loaded {restored?.Count} products.");
}
✅ A reusable save/load pattern
This "serialize → WriteAllText" and "ReadAllText → deserialize" round-trip is how a huge number of apps persist their state: settings, saved games, to-do lists, caches. You now have a general-purpose way to save any object graph to disk and bring it back.
Common Gotchas
⚠️ Only public properties are included
By default, serialization uses public properties. Private fields and non-public members are ignored. If a value isn't showing up in your JSON, check that it's a public property.
⚠️ Deserialization needs a way to construct the object
To rebuild an object, the serializer needs either a parameterless constructor or a constructor whose parameters match the JSON property names. Records work out of the box because their positional parameters line up with their properties — another reason records are handy for data. A class with only a custom multi-argument constructor may need a parameterless one added.
💡 Name matching is case-insensitive by default on read
When deserializing, System.Text.Json matches property names case-insensitively by default, so JSON "name" maps to a C# Name property. Still, keep your serialize/deserialize options consistent to avoid surprises.
Exercise & Quiz
🏋️ Exercise: A JSON-Backed Task List
Objective: Round-trip a list of objects through JSON and a file.
Instructions:
- Create a new project called
JsonTasks. - Define
record TaskItem(string Title, bool IsDone);. - Create a
List<TaskItem>with a few tasks. - Serialize it (with
WriteIndented = true) and save totasks.jsonusingFile.WriteAllText. - Read the file back, deserialize into a new
List<TaskItem>, and print each task's title and status. - Bonus: wrap the load in
try/catch (JsonException).
Starter Code:
using System.Text.Json;
record TaskItem(string Title, bool IsDone);
var tasks = new List<TaskItem>
{
new TaskItem("Learn JSON", true),
new TaskItem("Build an app", false)
};
string path = "tasks.json";
var options = new JsonSerializerOptions { WriteIndented = true };
// TODO: serialize tasks and write to path
// TODO: read path, deserialize, and print each task
💡 Hint
Save: File.WriteAllText(path, JsonSerializer.Serialize(tasks, options));. Load: var loaded = JsonSerializer.Deserialize<List<TaskItem>>(File.ReadAllText(path));, then foreach over loaded (guard for null).
✅ Solution
using System.Text.Json;
record TaskItem(string Title, bool IsDone);
var tasks = new List<TaskItem>
{
new TaskItem("Learn JSON", true),
new TaskItem("Build an app", false)
};
string path = "tasks.json";
var options = new JsonSerializerOptions { WriteIndented = true };
// Save
File.WriteAllText(path, JsonSerializer.Serialize(tasks, options));
Console.WriteLine("Saved tasks.json");
// Load
try
{
string text = File.ReadAllText(path);
var loaded = JsonSerializer.Deserialize<List<TaskItem>>(text);
foreach (var t in loaded ?? new List<TaskItem>())
{
string mark = t.IsDone ? "[x]" : "[ ]";
Console.WriteLine($"{mark} {t.Title}");
}
}
catch (JsonException ex)
{
Console.WriteLine($"Invalid JSON: {ex.Message}");
}
Output:
Saved tasks.json
[x] Learn JSON
[ ] Build an app
And tasks.json on disk contains readable, indented JSON.
🎯 Quick Quiz
Question 1: What does serialization do?
Question 2: Which option makes serialized JSON human-readable with indentation?
Question 3: By default, which members does System.Text.Json serialize?
Summary
🎉 Key Takeaways
- Serialization turns an object into text (JSON); deserialization rebuilds it. Tools live in
System.Text.Json. JsonSerializer.Serialize(obj)produces JSON;JsonSerializer.Deserialize<T>(json)reconstructs an object (result is nullable; malformed JSON throwsJsonException).- Records, classes, and collections all serialize; only public properties are included by default.
- Control output with
JsonSerializerOptions—WriteIndentedandPropertyNamingPolicy = CamelCaseare the common ones. - Combine with file I/O (Lesson 4.1): serialize →
WriteAllText, andReadAllText→ deserialize for a full save/load.
📚 Additional Resources
- System.Text.Json overview — Microsoft Docs
- How to serialize and deserialize JSON
- JsonSerializer — API reference
🚀 What's Next?
You can save and load structured data locally. But modern apps also fetch data over the internet — often as JSON. In Lesson 4.3: Asynchronous Programming, you'll learn async/await, the key to doing slow work (like network and file access) without freezing your app.
🎉 Structured data, saved!
Your objects can now live on disk and travel between systems. Next: doing slow work without freezing.