C# Files and JSON

Read and write text files, process large files line by line, build paths that work on every operating system, manage folders, and save and load objects as JSON with System.Text.Json.

Lesson 22 of 43Advanced17 min read

Most programs need to keep data after they close: settings, saved games, reports, logs, exported orders. The simplest place to keep it is a file. .NET's System.IO namespace has everything you need to read and write files, work with folders and build file paths that work on Windows, macOS and Linux.

For structured data, the standard format today is JSON. It's readable by people, understood by every programming language, and used by almost every web API. .NET's built-in System.Text.Json library turns C# objects into JSON text (serialisation) and JSON text back into objects (deserialisation) in a single line, and it's fast enough for high-traffic web servers.

In this lesson you'll read and write whole files and individual lines, process large files without loading them all into memory, handle the errors that file access can cause, and save and load objects as JSON with options for formatting and naming.

Reading and writing text files

The static File class covers most needs:

MethodWhat it does
File.WriteAllText(path, text)Creates or overwrites a file with text
File.ReadAllText(path)Reads the whole file into a string
File.WriteAllLines(path, lines)Writes each string as a line
File.ReadAllLines(path)Reads every line into a string array
File.AppendAllText(path, text)Adds text to the end, creating the file if needed
File.Exists(path)Checks whether a file exists
File.ReadLines(path)Reads lines one at a time, for large files

Each has an async version, such as await File.ReadAllTextAsync(path), which is what servers and apps with a user interface should use.

Paths and folders

Never join paths with + and hard-coded slashes. Path.Combine("data", "2026", "orders.csv") uses the right separator for the operating system.

  • Path.GetFileName, Path.GetExtension and Path.GetFileNameWithoutExtension take paths apart.
  • Directory.CreateDirectory(path) creates a folder and any missing parents, and does nothing if it already exists.
  • Directory.EnumerateFiles(folder, "*.csv") lists matching files.
  • Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) finds the right place for an app's settings.
  • Relative paths are relative to the current working directory, which isn't always the program's folder; AppContext.BaseDirectory gives the program's own folder.

JSON with System.Text.Json

C#
using System.Text.Json;

var settings = new Settings("dark", 14, ["en", "fr"]);
string json = JsonSerializer.Serialize(settings);
// {"Theme":"dark","FontSize":14,"Languages":["en","fr"]}

Settings? loaded = JsonSerializer.Deserialize<Settings>(json);

record Settings(string Theme, int FontSize, List<string> Languages);
  • Public properties are written as JSON properties; records and classes both work.
  • new JsonSerializerOptions { WriteIndented = true } produces readable, indented output.
  • JsonSerializerDefaults.Web uses camelCase names and case-insensitive reading, matching most web APIs.
  • [JsonPropertyName("first_name")] maps a property to a different JSON name, and [JsonIgnore] leaves one out.
  • Deserialisation throws a JsonException for invalid JSON, so wrap it in try when the data comes from outside.

C# Advanced Syntax

File and JSON syntax

File.WriteAllText("note.txt", "Hello");
string text = File.ReadAllText("note.txt");
File.AppendAllText("log.txt", $"{DateTime.Now:O} started\n");
foreach (string line in File.ReadLines("big.csv")) { }
string path = Path.Combine(AppContext.BaseDirectory, "data", "orders.json");
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
await File.WriteAllTextAsync(path, json);
var opts = new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true };
string json = JsonSerializer.Serialize(order, opts);
Order? o = JsonSerializer.Deserialize<Order>(json, opts);

C# Advanced Summary

  • File.ReadAllText, WriteAllText, ReadAllLines and AppendAllText handle most text files.
  • File.ReadLines streams large files line by line; use the Async versions in apps and servers.
  • Build paths with Path.Combine; create folders with Directory.CreateDirectory.
  • JsonSerializer.Serialize and Deserialize convert between objects and JSON.
  • JsonSerializerOptions control indentation and naming; expect IOException and JsonException.

C# Advanced Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Writing, appending and reading lines

C#
string folder = Path.Combine(Path.GetTempPath(), "codevra-demo");
Directory.CreateDirectory(folder);
string path = Path.Combine(folder, "visits.log");

File.WriteAllLines(path, new[] { "2026-03-01 home", "2026-03-01 shop" });
File.AppendAllText(path, "2026-03-02 cart" + Environment.NewLine);

string[] lines = File.ReadAllLines(path);
Console.WriteLine($"{lines.Length} lines");
foreach (string line in lines)
{
    string[] parts = line.Split(' ');
    Console.WriteLine($"{parts[1],-5} on {parts[0]}");
}

Console.WriteLine(Path.GetFileName(path));
Console.WriteLine(Path.GetExtension(path));
Console.WriteLine(File.Exists(Path.Combine(folder, "missing.txt")));
Output
3 lines
home  on 2026-03-01
shop  on 2026-03-01
cart  on 2026-03-02
visits.log
.log
False
Line-by-line explanation
  1. Line 1Path.Combine builds a path with the right separator for the operating system.
  2. Line 2CreateDirectory does nothing if the folder already exists.
  3. Line 5WriteAllLines creates or overwrites the file.
  4. Line 6AppendAllText adds to the end; Environment.NewLine is the system's line ending.
  5. Line 8ReadAllLines returns one string per line.
  6. Lines 16 to 17Path methods split a path into its parts.
Example 2

Saving and loading settings as JSON

C#
using System.Text.Json;

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true };
string path = Path.Combine(Path.GetTempPath(), "codevra-settings.json");

var settings = new AppSettings { Theme = "dark", FontSize = 16, Recent = ["report.csv", "notes.txt"] };
File.WriteAllText(path, JsonSerializer.Serialize(settings, options));
Console.WriteLine(File.ReadAllText(path));

var loaded = JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(path), options)!;
loaded.FontSize += 2;
Console.WriteLine($"{loaded.Theme}, size {loaded.FontSize}, {loaded.Recent.Count} recent files");

class AppSettings
{
    public string Theme { get; set; } = "light";
    public int FontSize { get; set; } = 14;
    public List<string> Recent { get; set; } = [];
}
Output
{
  "theme": "dark",
  "fontSize": 16,
  "recent": [
    "report.csv",
    "notes.txt"
  ]
}
dark, size 18, 2 recent files
Line-by-line explanation
  1. Line 3Web defaults use camelCase names; WriteIndented makes the file easy to read.
  2. Line 7Serialize turns the object into JSON text, which is written to the file.
  3. Line 10Deserialize reads the JSON back into a new AppSettings object.
  4. Line 11The loaded object is a normal object you can change.
  5. Lines 16 to 18Default values are used for any property missing from the file.
Example 3

Processing a large CSV safely

C#
string path = Path.Combine(Path.GetTempPath(), "sales.csv");
File.WriteAllLines(path, new[]
{
    "region,amount",
    "North,120.50",
    "South,80.00",
    "North,not-a-number",
    "East,45.25",
    "South,19.75"
});

var totals = new Dictionary<string, decimal>();
int skipped = 0;

try
{
    foreach (string line in File.ReadLines(path).Skip(1))
    {
        string[] cols = line.Split(',');
        if (cols.Length != 2 || !decimal.TryParse(cols[1], out decimal amount))
        {
            skipped++;
            continue;
        }
        totals[cols[0]] = totals.GetValueOrDefault(cols[0]) + amount;
    }
}
catch (IOException ex)
{
    Console.WriteLine($"Couldn't read the file: {ex.Message}");
}

foreach (var (region, total) in totals.OrderByDescending(t => t.Value))
    Console.WriteLine($"{region,-6}{total,8:F2}");
Console.WriteLine($"Skipped {skipped} bad line");
Output
North   120.50
South    99.75
East     45.25
Skipped 1 bad line
Line-by-line explanation
  1. Line 17ReadLines reads one line at a time, so even a huge file uses little memory; Skip(1) drops the header.
  2. Line 20Bad rows are counted and skipped instead of stopping the whole import.
  3. Line 25GetValueOrDefault starts each region at 0.
  4. Lines 28 to 31File problems such as a locked file are reported clearly.
  5. Line 33The regions are listed from highest total to lowest.

Common C# Advanced Mistakes

Building paths with string concatenation

Don't
string path = folder + "\\" + "orders.json";   // breaks on macOS and Linux
Do
string path = Path.Combine(folder, "orders.json");

Loading a huge file into memory

Don't
string[] all = File.ReadAllLines("server.log");   // 4 GB file, 4 GB of memory
Do
foreach (string line in File.ReadLines("server.log"))
{
    if (line.Contains("ERROR")) Console.WriteLine(line);
}

Trusting JSON from outside

Don't
var order = JsonSerializer.Deserialize<Order>(body)!;   // crashes on bad JSON or a null body
Do
try
{
    var order = JsonSerializer.Deserialize<Order>(body);
    if (order is null) return;
}
catch (JsonException)
{
    Console.WriteLine("The data isn't valid JSON.");
}

C# Advanced in Real Projects

Files and JSON in real apps

ASP.NET Core reads its configuration from appsettings.json, and every API request and response body is JSON handled by System.Text.Json. Desktop apps save user settings as JSON in the application data folder. Import jobs stream large CSV and log files line by line with File.ReadLines, and export features write reports with File.WriteAllTextAsync.

C# Advanced FAQ

How do I read a text file line by line in C#?

Use foreach (string line in File.ReadLines(path)), which reads one line at a time and keeps memory use low even for huge files. File.ReadAllLines(path) loads every line into an array at once, which is fine for small files. For async code, File.ReadLinesAsync is available in .NET 7 and later.

How do I check if a file exists in C#?

Call File.Exists(path), which returns true or false and never throws. For folders, use Directory.Exists(path). Remember that a file can still disappear or be locked between the check and your read, so keep a try/catch for IOException around the actual operation.

How do I pretty-print JSON in C#?

Pass options with WriteIndented = true: JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true }). Create the options object once and reuse it, rather than creating new options for every call, because the serializer caches information per options instance.

Why are my JSON properties null after deserializing?

The usual cause is a name mismatch: System.Text.Json matches property names case-sensitively by default, so "name" in the JSON won't fill Name. Use JsonSerializerDefaults.Web or set PropertyNameCaseInsensitive = true, or map names with [JsonPropertyName]. Also check that the properties are public and have a setter, an init accessor or a matching constructor parameter.

What is the difference between Newtonsoft.Json and System.Text.Json?

System.Text.Json is built into .NET, fast and the default in ASP.NET Core. Newtonsoft.Json (Json.NET) is an older third-party library with more features and looser defaults, and it's still common in existing projects. For new code, start with System.Text.Json and switch only if you need a feature it lacks.

C# Advanced Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the line to read the whole file into a string.

string text = File.("notes.txt");
Multiple choiceeasy

Which method adds text to the end of a file without removing what's there?

Multiple choicemedium

Why use Path.Combine instead of joining strings with a slash?

Predict the outputmedium

What does this print?

var json = JsonSerializer.Serialize(new { Name = "Ada", Age = 36 });
Console.WriteLine(json);
Multiple choicehard

Why is File.ReadLines better than File.ReadAllLines for a very large file?

C# Advanced Quiz

5 questions to check what you learned.

  1. Which namespace contains File, Path and Directory?

    • System.IO
    • System.Text
    • System.Files
    • System.Data
  2. What does JsonSerializer.Deserialize<T> do?

    • Turns JSON text into an object of type T
    • Writes JSON to a file
    • Validates a file path
    • Turns an object into JSON
  3. What does WriteIndented = true change?

    • Property names
    • Numbers are rounded
    • JSON is written over several indented lines, easier for people to read
    • Null values are removed
  4. What does Directory.CreateDirectory do if the folder already exists?

    • Throws an exception
    • Nothing; it simply returns
    • Deletes and recreates it
    • Creates a numbered copy
  5. Which attribute maps a property to a different JSON name?

    • [JsonIgnore]
    • [Name("...")]
    • [Serializable]
    • [JsonPropertyName("...")]