Skip to main content

📝 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:

graph LR A["C# object
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:

  1. Create a new project called JsonTasks.
  2. Define record TaskItem(string Title, bool IsDone);.
  3. Create a List<TaskItem> with a few tasks.
  4. Serialize it (with WriteIndented = true) and save to tasks.json using File.WriteAllText.
  5. Read the file back, deserialize into a new List<TaskItem>, and print each task's title and status.
  6. 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 throws JsonException).
  • Records, classes, and collections all serialize; only public properties are included by default.
  • Control output with JsonSerializerOptionsWriteIndented and PropertyNamingPolicy = CamelCase are the common ones.
  • Combine with file I/O (Lesson 4.1): serialize → WriteAllText, and ReadAllText → deserialize for a full save/load.

📚 Additional Resources

🚀 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.