Skip to content

Intermediate · Exceptions, delegates, events and async · topic 30 of 43 · 30 min

Files and JSON with System.Text.Json

Explanation

The File class reads and writes whole files in one line (File.ReadAllTextAsync, File.WriteAllTextAsync), and Path.Combine builds file paths that work on every operating system. JSON is the text format most APIs and apps use to store and exchange data. System.Text.Json turns objects into JSON with JsonSerializer.Serialize and back with JsonSerializer.Deserialize<T>, and JsonSerializerOptions controls details such as indentation and camelCase names.

Why files and JSON?

Everything in variables disappears when the program ends. To keep data, such as a list of expenses or app settings, you save it to a file. JSON (JavaScript Object Notation) is a simple text format of names and values in curly braces and lists in square brackets. Almost every web API speaks JSON, so learning it serves you everywhere.

json
{
  "name": "Asha",
  "city": "Pune",
  "marks": [91, 85, 78],
  "isActive": true
}
A JSON object with a string, a list of numbers and a bool.

Reading and writing text files

csharp
string folder = Path.Combine(Environment.CurrentDirectory, "data");
Directory.CreateDirectory(folder);                 // does nothing if it exists
string file = Path.Combine(folder, "notes.txt");

await File.WriteAllTextAsync(file, "First line\n");
await File.AppendAllTextAsync(file, "Second line\n");

if (File.Exists(file))
{
    string[] lines = await File.ReadAllLinesAsync(file);
    Console.WriteLine($"{lines.Length} lines; first: {lines[0]}");
}
Path.Combine uses the right separator (\ on Windows, / on Linux). The Async versions do not block a thread while the disk works.

A relative path such as "data/notes.txt" is relative to the current working directory: the folder you ran the program from, which for dotnet run is usually the project folder. If the file "cannot be found" but you can see it, print Environment.CurrentDirectory to check where the program is looking.

Big files: read line by line

csharp
int count = 0;
foreach (string line in File.ReadLines("big-log.txt"))   // reads lazily, one line at a time
{
    if (line.Contains("ERROR")) count++;
}
Console.WriteLine($"{count} errors");
ReadAllLines loads the whole file into memory. File.ReadLines streams it, which suits files of hundreds of MB.

Objects to JSON: Serialize

csharp
using System.Text.Json;

var expense = new Expense("Groceries", 1240.5m, new DateOnly(2026, 3, 14));
string json = JsonSerializer.Serialize(expense);
Console.WriteLine(json);
// {"Title":"Groceries","Amount":1240.5,"Date":"2026-03-14"}

public record Expense(string Title, decimal Amount, DateOnly Date);
System.Text.Json is built into .NET; no package needed. By default, property names are written as they are in C# (PascalCase).

JSON to objects: Deserialize

csharp
string text = """{"Title":"Rent","Amount":15000,"Date":"2026-04-01"}""";
Expense? e = JsonSerializer.Deserialize<Expense>(text);
Console.WriteLine($"{e?.Title}: {e?.Amount}");   // Rent: 15000
Deserialize returns T? because the JSON text could be the word null. Invalid JSON throws JsonException.

Options: indentation and camelCase

Most web APIs use camelCase names ("title", not "Title"). By default, deserializing is case-sensitive, so camelCase JSON would NOT fill your PascalCase properties; they would silently stay empty. Set options to match. Create the options object once and reuse it, because .NET caches work per options instance.

csharp
var options = new JsonSerializerOptions
{
    WriteIndented = true,                                // pretty, multi-line output
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,   // Title -> "title"
    PropertyNameCaseInsensitive = true                   // accept "title" or "Title"
};

string pretty = JsonSerializer.Serialize(expense, options);
Expense? back = JsonSerializer.Deserialize<Expense>(pretty, options);
ASP.NET Core already uses camelCase and case-insensitive reading for its APIs (the "web defaults").

Attributes for special cases

csharp
using System.Text.Json.Serialization;

public class Customer
{
    [JsonPropertyName("full_name")] public string Name { get; set; } = "";
    [JsonIgnore] public string PasswordHash { get; set; } = "";   // never written to JSON
}
[JsonPropertyName] sets the exact JSON name. [JsonIgnore] leaves a property out.

Saving and loading a list

csharp
var list = new List<Expense> { new("Tea", 20m, new DateOnly(2026, 3, 1)) };
await File.WriteAllTextAsync("expenses.json", JsonSerializer.Serialize(list, options));

string saved = await File.ReadAllTextAsync("expenses.json");
var loaded = JsonSerializer.Deserialize<List<Expense>>(saved, options) ?? new List<Expense>();
Console.WriteLine(loaded.Count);   // 1
Use ?? new List<Expense>() so the rest of the code never handles null.

Common mistakes and fixes

  • camelCase JSON into PascalCase properties without options: values stay default. Use PropertyNameCaseInsensitive = true or JsonSerializerDefaults.Web.
  • Saving secrets (passwords, API keys) into JSON files that get committed to Git: never. Use [JsonIgnore] and proper secret storage.
  • Hard-coding "C:\Users\..." paths: they break on other machines and Linux. Use Path.Combine and relative or configured paths.
  • Forgetting that a file may not exist on first run: check File.Exists and start with an empty list.
  • Mixing up System.Text.Json and the older Newtonsoft.Json library: both are popular, but their attributes and options differ. Check which one a project uses.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Files and JSON with System.Text.Json · 1/7

Files and JSON

In this lesson your program will save data to files and use JSON.

Prefer a video by a teacher? Find free videos on “C# System.Text.Json serialize deserialize read write file async tutorial” on YouTube ↗

In real life: A personal expense tracker that remembers your data

A student in Kolkata tracks monthly spending in a small console app. Each run loads expenses.json (or starts empty), adds the new expense, prints the month's total, and saves the file back.

csharp
using System.Text.Json;

const string FilePath = "expenses.json";
var options = new JsonSerializerOptions { WriteIndented = true, PropertyNameCaseInsensitive = true };

List<Expense> expenses = File.Exists(FilePath)
    ? JsonSerializer.Deserialize<List<Expense>>(await File.ReadAllTextAsync(FilePath), options) ?? new()
    : new();

expenses.Add(new Expense("Metro pass", 500m, DateOnly.FromDateTime(DateTime.Today)));

decimal monthTotal = expenses
    .Where(e => e.Date.Month == DateTime.Today.Month && e.Date.Year == DateTime.Today.Year)
    .Sum(e => e.Amount);
Console.WriteLine($"{expenses.Count} expenses saved. This month: Rs {monthTotal}");

await File.WriteAllTextAsync(FilePath, JsonSerializer.Serialize(expenses, options));

public record Expense(string Title, decimal Amount, DateOnly Date);
Run it a few times: the count grows because data is saved between runs.

Practice (0/4 done)

Task 1 · Question

record P(string Name, int Age); What does JsonSerializer.Serialize(new P("Ravi", 30)) produce with default options?

Task 2 · Question

JSON {"name":"Ravi"} is deserialized into a class with property Name using default options. What is Name?

Task 3 · Question

Which is best for counting lines that contain "ERROR" in a 2 GB log file?

Task 4 · Practice

Write a console app that keeps a contacts list in contacts.json. On start, load the list (or start empty if the file is missing), add a contact typed by the user (name and phone), print all contacts, and save the list with indented camelCase JSON.

Quick quiz

Question 1

1.Why use Path.Combine("data", "a.txt") instead of "data\\a.txt"?

Question 2

2.What does JsonSerializer.Deserialize<T> throw for text that is not valid JSON?

Question 3

3.Which option makes the JSON output multi-line and readable?

Question 4

4.What does [JsonIgnore] do?

Question 5

5.Is System.Text.Json part of .NET or a separate download?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Why use Path.Combine("data", "a.txt") instead of "data\\a.txt"?

Answer: It uses the correct separator on every operating system

Windows uses \ and Linux/macOS use /; Path.Combine handles it.

What does JsonSerializer.Deserialize<T> throw for text that is not valid JSON?

Answer: JsonException

Invalid JSON raises JsonException.

Which option makes the JSON output multi-line and readable?

Answer: WriteIndented

WriteIndented = true pretty-prints the output.

What does [JsonIgnore] do?

Answer: Leaves the property out of the JSON

The property is skipped when serializing and deserializing.

Is System.Text.Json part of .NET or a separate download?

Answer: Built into .NET

System.Text.Json ships with modern .NET.

Which feature must be enabled on serde for #[derive(Serialize)]?Related

Answer: derive

cargo add serde --features derive.

From Serde and JSON

What happens when JSON.parse gets broken text?Related

Answer: It throws a SyntaxError

JSON.parse throws on bad input, so wrap it in try and catch.

From JSON: the language of APIs

Which character is NOT allowed for indentation in YAML?Related

Answer: Tab

YAML forbids tabs for indentation. Use spaces.

From YAML in depth

Which value is valid JSON?Related

Answer: {"up": true}

JSON needs double quotes, lower-case true and no trailing comma.

From REST APIs and JSON

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Exceptions, delegates, events and async”. The module exam comes next.
  2. Module 7 exam: Exceptions, delegates, events and async · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Web APIs with ASP.NET Core and EF Core. 43 topics left in the course before the final exam.

← Previous: async and await: waiting without blocking