How to Stream JSON Lines With System.Text.Json in .NET 11

csharpdotnetjson
How to Stream JSON Lines With System.Text.Json in .NET 11

.NET 11 adds JsonSerializer.SerializeAsyncEnumerable for writing an async sequence as JSON Lines. Pass topLevelValues: true and each item becomes a separate UTF-8 JSON value followed by a line feed. A worker on .NET 9 or later can read the same file with DeserializeAsyncEnumerable and process the records one at a time.

What Goes Into a JSON Lines File?

This JSON Lines file contains two dates from Microsoft's .NET release history. You can copy either line into a JSON parser by itself.

{"version":"10.0","shipped":"2025-11-11"}
{"version":"11.0","shipped":"2026-11-10"}

The final record also ends in LF (0A in hexadecimal), including on Windows. The JSON Lines writer ignores WriteIndented and NewLine in this mode so a nested object still occupies one physical line. A newline inside a string is escaped as part of the JSON value.

Write the File From an Async Sequence

Create a console app and save the following two rows as releases.csv in its directory. The input for this example has two fields and no header.

dotnet new console -n ReleaseLines -f net11.0
cd ReleaseLines
printf '10.0,2025-11-11\n11.0,2026-11-10\n' > releases.csv

Replace Program.cs with this exporter and reader. The source reads one CSV row at a time. yield return hands each release to the serializer as soon as the date has been parsed.

using System.Globalization;
using System.Text.Json;

var options = JsonSerializerOptions.Web;

if (args is ["write"])
{
    using var output = File.Create("releases.jsonl");
    await JsonSerializer.SerializeAsyncEnumerable(
        output, ReadReleases("releases.csv"), topLevelValues: true, options: options);
}
else if (args is ["read", var path])
{
    using var input = File.OpenRead(path);
    await foreach (var release in JsonSerializer.DeserializeAsyncEnumerable<Release>(
        input, topLevelValues: true, options: options))
    {
        Console.WriteLine($".NET {release.Version} shipped {release.Shipped:yyyy-MM-dd}");
    }
}
else
{
    Console.Error.WriteLine("Use 'write' or 'read <input-file>'.");
    return 1;
}

return 0;

static async IAsyncEnumerable<Release> ReadReleases(string path)
{
    await foreach (string line in File.ReadLinesAsync(path))
    {
        var fields = line.Split(',');
        yield return new Release(fields[0],
            DateOnly.ParseExact(fields[1], "yyyy-MM-dd", CultureInfo.InvariantCulture));
    }
}

readonly record struct Release(string Version, DateOnly Shipped);

The two calls use the same web naming policy, so Release.Version maps to "version". Run the commands from the project directory. The write command creates or replaces releases.jsonl.

dotnet run -- write
dotnet run -- read releases.jsonl
.NET 10.0 shipped 2025-11-11
.NET 11.0 shipped 2026-11-10

The writer also accepts a PipeWriter and has overloads that take a generated JsonTypeInfo<T> contract. If the receiving application expects an array, omit topLevelValues: true on both calls.

Does the Reader Enforce One Record Per Line?

Try replacing the line feeds with spaces and run the same reader.

tr '\n' ' ' < releases.jsonl > single-line.json
dotnet run -- read single-line.json

Both releases still print. The stream overload added in .NET 9 reads a sequence of top-level values separated by JSON whitespace. Two objects on the same line with a space between them satisfy that rule.

Suppose your importer has to tell the sender that line 17 failed. Read the file line by line and deserialize each line separately. Then you know which line failed, and you can keep reading if the sender accepts partial imports.

What Happens If an Export Stops Early?

A JSON Lines file can contain well-formed records even if the producer stops halfway through a batch. The three valid releases at the start look the same whether the export finished there or crashed just before the fourth. For a batch handoff, write to a temporary file in the destination directory and rename it after serialization and disposal succeed.

Streaming saves you from buffering the whole file. The consumer still needs enough memory to deserialize the largest string in a record. If you control the feed, agree on a maximum record size with the importer. For a large completed export, Zstandard compression in .NET 11 shows how to compress the file in chunks.

Summary

For a .NET worker reading a trusted export, await foreach over DeserializeAsyncEnumerable is enough. Use a line reader when you need to reject two values on the same line or tell the sender which line to fix.

Frequently Asked Questions

How do you write JSON Lines in .NET 11?

Call JsonSerializer.SerializeAsyncEnumerable with an IAsyncEnumerable, an output Stream or PipeWriter, and topLevelValues set to true. The writer emits a compact UTF-8 JSON value followed by a line feed for each element.

Does the JSON Lines writer honor WriteIndented or NewLine?

In top-level-values mode it always uses compact JSON and terminates each record with LF, including the last record. Pretty-printing options and platform-specific newline settings do not change those bytes.

Can .NET 10 read a JSON Lines file produced by .NET 11?

Yes. The Stream overload of DeserializeAsyncEnumerable that accepts topLevelValues has been available since .NET 9. Set it to true and choose a type and naming policy that match the producer.

Browse all .NET guides →
Loading comments...

Whenever you're ready, there are 5 ways I can help you:

  1. Pragmatic Self-Hosting: Learn to deploy and run your apps on a VPS with Dokploy and Tailscale. Keep administration private, automate releases, and practice restoring backups, with source code and a runbook you can reuse.
  2. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  3. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  4. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  5. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

One practical .NET tip, every Saturday.

Join 66,000+ engineers who read it. Free, and you can unsubscribe any time.