C# Fundamentals: From Your First Program to Async and JSON · Modern C# in Practice: Generics, Async, Errors and JSON
Files, JSON and Dates
Read and write files, serialize objects to and from JSON with System.Text.Json, control naming and indentation, and handle dates without time-zone surprises.
What you'll build
A settings store. It serializes a typed AppSettings record to JSON, writes it to a temporary file, reads it back, parses an untrusted JSON document, and does simple date arithmetic with DateOnly.
The idea
Files: System.IO.File covers most needs: ReadAllText, WriteAllText, ReadLines (streams line by line, good for big files), AppendAllText, Exists, and async versions (ReadAllTextAsync). Build paths with Path.Combine, not string concatenation, and use Path.GetTempPath() for scratch files.
JSON is handled by System.Text.Json (built in):
JsonSerializer.Serialize(value)andJsonSerializer.Deserialize<T>(json)convert between objects and text.JsonSerializerOptionsconfigure naming (PropertyNamingPolicy = JsonNamingPolicy.CamelCase), indentation (WriteIndented), and case-insensitive reading (PropertyNameCaseInsensitive). Create one and reuse it.- Records work well as data transfer types. Properties map by name.
JsonDocument.Parsereads JSON without a class, for exploring or partial reading.System.Text.JsonthrowsJsonExceptionon malformed JSON; treat file and network content as untrusted.
Dates: DateTime mixes dates, times and kinds. Prefer DateOnly for calendar dates, TimeOnly for times of day, and DateTimeOffset for moments in time (it includes the UTC offset). Store and exchange instants in UTC.
The code
using System.Text.Json;
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var settings = new AppSettings("Inventory", 8080, ["exports", "audit"], new DateOnly(2026, 1, 15));
// Object -> JSON text
var json = JsonSerializer.Serialize(settings, options);
Console.WriteLine(json);
// Write to a file and read it back
var path = Path.Combine(Path.GetTempPath(), $"settings-{Guid.NewGuid():N}.json");
await File.WriteAllTextAsync(path, json);
var text = await File.ReadAllTextAsync(path);
var loaded = JsonSerializer.Deserialize<AppSettings>(text, options);
Console.WriteLine($"round trip equal: {loaded == settings}");
File.Delete(path);
Console.WriteLine($"file deleted: {!File.Exists(path)}");
// Untrusted input: parse defensively
const string incoming = @"{ ""name"": ""Billing"", ""port"": ""not-a-number"" }";
try
{
JsonSerializer.Deserialize<AppSettings>(incoming, options);
}
catch (JsonException)
{
Console.WriteLine("rejected malformed settings");
}
// Read pieces of a document without a class
using var document = JsonDocument.Parse(@"{ ""user"": { ""name"": ""Ada"", ""roles"": [""admin"", ""editor""] } }");
var user = document.RootElement.GetProperty("user");
Console.WriteLine($"{user.GetProperty("name").GetString()} has {user.GetProperty("roles").GetArrayLength()} roles");
// Dates: DateOnly for calendar dates
var due = settings.Created.AddMonths(1).AddDays(3);
Console.WriteLine($"due {due:yyyy-MM-dd} ({due.DayOfWeek}), {due.DayNumber - settings.Created.DayNumber} days after creation");
Console.WriteLine(DateOnly.TryParse("2026-02-30", out _) ? "valid date" : "2026-02-30 is not a valid date");
// Lines from a file one at a time
var lines = Path.Combine(Path.GetTempPath(), $"lines-{Guid.NewGuid():N}.txt");
File.WriteAllLines(lines, ["alpha", "beta", "", "gamma"]);
var nonEmpty = File.ReadLines(lines).Where(l => l.Length > 0).Select(l => l.ToUpper());
Console.WriteLine(string.Join(", ", nonEmpty));
File.Delete(lines);
record AppSettings(string Name, int Port, string[] Features, DateOnly Created)
{
// Arrays are compared by reference, so compare contents for the round trip check
public virtual bool Equals(AppSettings? other) =>
other is not null && Name == other.Name && Port == other.Port && Created == other.Created && Features.SequenceEqual(other.Features);
public override int GetHashCode() => HashCode.Combine(Name, Port, Created);
}
Output
{
"name": "Inventory",
"port": 8080,
"features": [
"exports",
"audit"
],
"created": "2026-01-15"
}
round trip equal: True
file deleted: True
rejected malformed settings
Ada has 2 roles
due 2026-02-18 (Wednesday), 34 days after creation
2026-02-30 is not a valid date
ALPHA, BETA, GAMMARun this on your computer — see “Get Started” for setup.
Common mistakes
- Building paths with
+and slashes. UsePath.Combine; it handles separators per operating system. - Reading a huge file with
ReadAllText. Stream it withReadLinesor aStreamReader. - Creating a new
JsonSerializerOptionsfor every call. It is expensive; keep one static instance. - Using
DateTime.Nowfor stored values. Local time is ambiguous; store UTC (DateTime.UtcNow) orDateTimeOffset. - Trusting deserialized data. JSON can parse yet still be nonsense (a negative port); validate after deserializing.
Key points
FileandPathhandle file IO; prefer async methods for large or remote data.JsonSerializerplus a sharedJsonSerializerOptionsconverts records to and from JSON.JsonDocumentreads parts of a JSON document without defining classes.- Use
DateOnly,TimeOnlyandDateTimeOffset/UTC to avoid time-zone mistakes.
Further reading: System.Text.Json overview.
Practice questions
Why store timestamps in UTC or as `DateTimeOffset`?
Answer
Local time depends on the time zone and daylight saving rules, so the same value can mean different moments. UTC (or an offset-aware DateTimeOffset) identifies one exact instant, and you convert to local time only for display.
Why reuse a single `JsonSerializerOptions` instance?
Answer
System.Text.Json caches serialization metadata per options instance. Creating a new instance for every call discards that cache and is noticeably slower, so define one and reuse it.
Open in the app to mark lessons complete, take notes and track your progress.