<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <title>ABP.IO Stories</title>
    <link>https://abp.io/community/articles</link>
    <description>A hub for ABP Framework, .NET, and software development. Access articles, tutorials, news, and contribute to the ABP community.</description>
    <lastBuildDate>Wed, 23 Sep 2026 11:28:19 Z</lastBuildDate>
    <generator>Community - ABP.IO</generator>
    <image>
      <url>https://abp.io/assets/favicon.ico/favicon-32x32.png</url>
      <title>ABP.IO Stories</title>
      <link>https://abp.io/community/articles</link>
    </image>
    <a10:link rel="self" type="application/rss+xml" title="self" href="https://abp.io/community/rss?member=okankoca" />
    <item>
      <guid isPermaLink="true">https://abp.io/community/posts/system.text.json-in-.net-11-naming-policies-union-types-and-ndjson-streaming-ul2reiuf</guid>
      <link>https://abp.io/community/posts/system.text.json-in-.net-11-naming-policies-union-types-and-ndjson-streaming-ul2reiuf</link>
      <a10:author>
        <a10:name>okankoca</a10:name>
        <a10:uri>https://abp.io/community/members/okankoca</a10:uri>
      </a10:author>
      <category>openapi</category>
      <category>dotnet</category>
      <category>asp.net-core</category>
      <category>csharp</category>
      <category>.net</category>
      <title>System.Text.Json in .NET 11: Naming Policies, Union Types, and NDJSON Streaming</title>
      <description>Explore the key System.Text.Json improvements in .NET 11, including flexible naming policies, C# union type serialization, and NDJSON streaming. This article demonstrates these features with runnable ASP.NET Core examples and explains their impact on API contracts, frontend clients, OpenAPI, streaming, performance, and production adoption.</description>
      <pubDate>Mon, 21 Sep 2026 12:48:24 Z</pubDate>
      <a10:updated>2026-09-23T10:52:29Z</a10:updated>
      <content:encoded><![CDATA[<p><code>System.Text.Json</code> is getting several useful improvements in .NET 11 that go beyond simple serialization.</p>
<p>Three changes are particularly relevant for API developers:</p>
<ul>
<li>more flexible naming-policy customization,</li>
<li>serialization support for C# union types,</li>
<li>and NDJSON output for asynchronous streams.</li>
</ul>
<p>At first glance, these may look like unrelated serializer features. In practice, they affect three important parts of an API contract:</p>
<pre><code class="language-text">Naming policies → property names
Union types     → possible value shapes
NDJSON          → how values are delivered
</code></pre>
<p>That has direct consequences for ASP.NET Core APIs, TypeScript frontends, generated clients, large-result endpoints, and AI streaming scenarios.</p>
<p>In this article, we'll look at each feature, build a small API around them, and discuss when NDJSON is a better fit than a conventional JSON array.</p>
<p>.NET 11 includes other <code>System.Text.Json</code> work as well, but features such as <code>GetTypeInfo&lt;T&gt;</code>, type-level <code>JsonIgnore</code>, and closed-hierarchy inference are outside the scope of this article except where they clarify the union guidance.</p>
<hr />
<h2>.NET 11 Status First</h2>
<p>At the time of writing, .NET 11 is at <strong>Release Candidate 1</strong>. Microsoft released <code>.NET 11.0.0-rc.1</code> on September 8, 2026, and the .NET release metadata lists the channel in the <strong>Go-Live</strong> support phase.</p>
<p>An important RC1 change is that <strong>C# 15 is now the default language version for projects targeting .NET 11</strong>. Union types were stabilized for C# 15 in RC1, so a <code>net11.0</code> project no longer needs:</p>
<pre><code class="language-xml">&lt;LangVersion&gt;preview&lt;/LangVersion&gt;
</code></pre>
<p>This matters because some .NET 11 library documentation still contains older wording that calls C# unions a preview feature. For RC1 language status, the <a href="https://github.com/dotnet/core/blob/main/release-notes/11.0/preview/rc1/csharp.md">C# in .NET 11 RC1 release notes</a> are the more specific source.</p>
<p>RC1 is still pre-GA, however. Before a final production rollout, re-check the .NET 11 release notes, known issues, and serializer behavior against the final SDK.</p>
<hr />
<h2>Naming Policies Become More Flexible</h2>
<p><code>System.Text.Json</code> already supports built-in naming conventions such as camelCase, snake_case, and kebab-case.</p>
<p>.NET 11 adds:</p>
<pre><code class="language-csharp">JsonNamingPolicy.PascalCase
</code></pre>
<p>and introduces <code>JsonNamingPolicyAttribute</code>, which allows a naming policy to be applied to an individual property or field.</p>
<p>Per-member name overrides were already possible with <code>[JsonPropertyName(&quot;someName&quot;)]</code>. The .NET 11 addition is different: instead of hard-coding one literal JSON name, a member can opt into a naming <strong>policy</strong>, so the transformation remains policy-driven.</p>
<p>For example:</p>
<pre><code class="language-csharp">using System.Text.Json;
using System.Text.Json.Serialization;

var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.PascalCase
};

var response = new EventResponse
{
    EventName = &quot;UserRegistered&quot;,
    CreatedAtUtc =
        new DateTimeOffset(2026, 9, 18, 8, 30, 0, TimeSpan.Zero)
};

Console.WriteLine(
    JsonSerializer.Serialize(response, options));

public sealed class EventResponse
{
    [JsonNamingPolicy(JsonKnownNamingPolicy.CamelCase)]
    public string EventName { get; init; } = &quot;&quot;;

    public DateTimeOffset CreatedAtUtc { get; init; }
}
</code></pre>
<p>Actual compact output:</p>
<pre><code class="language-json">{&quot;eventName&quot;:&quot;UserRegistered&quot;,&quot;CreatedAtUtc&quot;:&quot;2026-09-18T08:30:00+00:00&quot;}
</code></pre>
<p>The serializer uses PascalCase globally, but <code>EventName</code> is explicitly kept in camelCase.</p>
<h3>Why does this matter for APIs?</h3>
<p>Because JSON naming is part of the wire contract.</p>
<p>These responses contain the same data:</p>
<pre><code class="language-json">{&quot;userId&quot;:12}
</code></pre>
<pre><code class="language-json">{&quot;UserId&quot;:12}
</code></pre>
<p>but they are not necessarily compatible from a client's perspective.</p>
<p>A TypeScript application might have:</p>
<pre><code class="language-ts">interface User {
    userId: number;
}
</code></pre>
<p>Changing the server to return <code>UserId</code> can break client mappings, generated SDKs, runtime validation, or tests without changing the underlying C# property at all.</p>
<p>So naming-policy changes should be treated as contract changes.</p>
<p>In ASP.NET Core, a global policy can be configured like this:</p>
<pre><code class="language-csharp">builder.Services.ConfigureHttpJsonOptions(options =&gt;
{
    options.SerializerOptions.PropertyNamingPolicy =
        JsonNamingPolicy.PascalCase;
});
</code></pre>
<p>The point of .NET 11 is not that PascalCase is suddenly preferable for web APIs. The useful part is having finer control when an API needs exceptions to its global convention.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-21-system-text-json-in-net11-naming-policies-union-types-ndjson-streaming/api_contract_flow.png" alt="C# model to JSON contract and frontend client flow" /></p>
<p><em>Figure: An illustrative contract-flow example showing how a server-side naming decision becomes part of the JSON and frontend contract. The runnable sample below intentionally uses PascalCase globally to demonstrate the new .NET 11 policy.</em></p>
<hr />
<h2>C# Union Types Meet System.Text.Json</h2>
<p>Another important .NET 11 improvement is serialization support for C# union types.</p>
<p>A union represents a value that can be one of a fixed set of cases.</p>
<p>For example:</p>
<pre><code class="language-csharp">public record OrderCreated(
    int OrderId,
    string Status);

public record ValidationFailure(
    string ErrorCode,
    string Message);

public union CreateOrderResult(
    OrderCreated,
    ValidationFailure);
</code></pre>
<p>A <code>CreateOrderResult</code> can now contain either:</p>
<pre><code class="language-csharp">CreateOrderResult result =
    new OrderCreated(125, &quot;created&quot;);
</code></pre>
<p>or:</p>
<pre><code class="language-csharp">CreateOrderResult result =
    new ValidationFailure(
        &quot;INVALID_QUANTITY&quot;,
        &quot;Quantity must be greater than zero.&quot;);
</code></pre>
<p>Pattern matching can then handle the known cases explicitly:</p>
<pre><code class="language-csharp">string message = result switch
{
    OrderCreated order =&gt;
        $&quot;Order {order.OrderId} created.&quot;,

    ValidationFailure failure =&gt;
        failure.Message
};
</code></pre>
<p>The compiler knows which cases belong to the union instead of treating the value as an arbitrary <code>object</code>.</p>
<hr />
<h3>Serializing a Union</h3>
<p>.NET 11's <code>System.Text.Json</code> recognizes C# unions through the new <code>JsonTypeInfoKind.Union</code> contract kind and can serialize the active case directly. The feature works with both reflection-based serialization and source generation.</p>
<p>For example:</p>
<pre><code class="language-csharp">CreateOrderResult result =
    new OrderCreated(125, &quot;created&quot;);

string json =
    JsonSerializer.Serialize(result);

Console.WriteLine(json);
</code></pre>
<p>Actual compact output:</p>
<pre><code class="language-json">{&quot;OrderId&quot;:125,&quot;Status&quot;:&quot;created&quot;}
</code></pre>
<p>The validation case produces:</p>
<pre><code class="language-json">{&quot;ErrorCode&quot;:&quot;INVALID_QUANTITY&quot;,&quot;Message&quot;:&quot;Quantity must be greater than zero.&quot;}
</code></pre>
<p>There is no artificial union wrapper or discriminator in the JSON. The active case becomes the actual representation.</p>
<p>That maps to a TypeScript union using the same wire-property casing:</p>
<pre><code class="language-ts">type CreateOrderResult =
    | {
        OrderId: number;
        Status: string;
      }
    | {
        ErrorCode: string;
        Message: string;
      };
</code></pre>
<p>Because the serialized union has no discriminator, the client narrows by shape, for example:</p>
<pre><code class="language-ts">function handle(result: CreateOrderResult) {
    if (&quot;OrderId&quot; in result) {
        console.log(result.Status);
    } else {
        console.error(result.Message);
    }
}
</code></pre>
<p>That is valid TypeScript, but an explicit discriminator can be easier to evolve when you control a brand-new polymorphic object contract.</p>
<hr />
<h3>A Note on Union Deserialization</h3>
<p>Serialization and deserialization are not identical concerns. When a union contains two object-shaped cases, <code>System.Text.Json</code> may not be able to determine the active case from the JSON token alone.</p>
<p>For structurally distinct objects, .NET 11 provides <code>JsonUnionTypeStructuralClassifier</code>:</p>
<pre><code class="language-csharp">[JsonUnion(
    TypeClassifier =
        typeof(JsonUnionTypeStructuralClassifier))]
public union CreateOrderResult(
    OrderCreated,
    ValidationFailure);
</code></pre>
<p>In this sample, <code>OrderCreated</code> and <code>ValidationFailure</code> expose different root-level properties, so the classifier can distinguish them. The behavior is verified in the <strong>Running and Testing the Sample</strong> section below. If cases are still ambiguous, an explicit custom classifier may be required.</p>
<hr />
<h3>OpenAPI and Generated Clients</h3>
<p>ASP.NET Core's OpenAPI support represents a C# union as an <code>anyOf</code> schema with one entry per case type.</p>
<p>Conceptually:</p>
<pre><code class="language-yaml">CreateOrderResult:
  anyOf:
    - $ref: &quot;#/components/schemas/OrderCreated&quot;
    - $ref: &quot;#/components/schemas/ValidationFailure&quot;
</code></pre>
<p>A client generator can then potentially produce:</p>
<pre><code class="language-ts">type CreateOrderResult =
    OrderCreated | ValidationFailure;
</code></pre>
<p>That is much better than exposing the response as an untyped <code>object</code>, but generated clients should still be tested because generators can differ in how they model <code>anyOf</code>. Also remember that union serialization itself does not add a discriminator, so generated frontend code may still need shape-based narrowing.</p>
<p><img src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-21-system-text-json-in-net11-naming-policies-union-types-ndjson-streaming/anyof_union_openapi.png" alt="Scalar OpenAPI view of the CreateOrderResult union" /></p>
<p><em>Figure: Scalar represents <code>CreateOrderResult</code> as an OpenAPI <code>anyOf</code> schema with <code>OrderCreated</code> and <code>ValidationFailure</code> alternatives.</em></p>
<hr />
<h2>Putting Everything Together in ASP.NET Core</h2>
<p>The following Minimal API demonstrates the naming-policy configuration, a union response, OpenAPI/Scalar setup, and an NDJSON streaming endpoint.</p>
<h3>Project file</h3>
<pre><code class="language-xml">&lt;Project Sdk=&quot;Microsoft.NET.Sdk.Web&quot;&gt;

  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net11.0&lt;/TargetFramework&gt;
    &lt;Nullable&gt;enable&lt;/Nullable&gt;
    &lt;ImplicitUsings&gt;enable&lt;/ImplicitUsings&gt;
  &lt;/PropertyGroup&gt;

  &lt;ItemGroup&gt;
    &lt;PackageReference
        Include=&quot;Microsoft.AspNetCore.OpenApi&quot;
        Version=&quot;11.0.0-rc.1.26425.128&quot; /&gt;
    &lt;PackageReference
        Include=&quot;Scalar.AspNetCore&quot;
        Version=&quot;2.17.4&quot; /&gt;
  &lt;/ItemGroup&gt;

&lt;/Project&gt;
</code></pre>
<h3><code>Program.cs</code></h3>
<pre><code class="language-csharp">using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.Options;
using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

builder.Services.ConfigureHttpJsonOptions(options =&gt;
{
    options.SerializerOptions.PropertyNamingPolicy =
        JsonNamingPolicy.PascalCase;
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

app.MapGet(&quot;/api/contracts/naming&quot;, () =&gt;
    new NamingResponse
    {
        EventName = &quot;UserRegistered&quot;,
        CreatedAtUtc = DateTimeOffset.UtcNow
    });

app.MapPost(&quot;/api/orders&quot;, (
    CreateOrderRequest request) =&gt;
{
    CreateOrderResult result =
        request.Quantity &lt;= 0
            ? new ValidationFailure(
                &quot;INVALID_QUANTITY&quot;,
                &quot;Quantity must be greater than zero.&quot;)
            : new OrderCreated(
                125,
                &quot;created&quot;);

    return TypedResults.Ok(result);
});

app.MapGet(&quot;/api/events/stream&quot;, async (
    HttpContext context,
    IOptions&lt;JsonOptions&gt; jsonOptions,
    CancellationToken cancellationToken) =&gt;
{
    context.Response.ContentType =
        &quot;application/x-ndjson; charset=utf-8&quot;;

    await JsonSerializer.SerializeAsyncEnumerable(
        context.Response.BodyWriter,
        GenerateEvents(cancellationToken),
        topLevelValues: true,
        options: jsonOptions.Value.SerializerOptions,
        cancellationToken: cancellationToken);
});

app.Run();

static async IAsyncEnumerable&lt;StreamEvent&gt; GenerateEvents(
    [EnumeratorCancellation]
    CancellationToken cancellationToken)
{
    for (var i = 1; i &lt;= 5; i++)
    {
        await Task.Delay(1000, cancellationToken);

        yield return new StreamEvent(
            i,
            $&quot;Chunk {i}&quot;,
            DateTimeOffset.UtcNow);
    }
}

public sealed class NamingResponse
{
    [JsonNamingPolicy(JsonKnownNamingPolicy.CamelCase)]
    public string EventName { get; init; } = &quot;&quot;;

    public DateTimeOffset CreatedAtUtc { get; init; }
}

public record CreateOrderRequest(
    int ProductId,
    int Quantity);

public record OrderCreated(
    int OrderId,
    string Status);

public record ValidationFailure(
    string ErrorCode,
    string Message);

public union CreateOrderResult(
    OrderCreated,
    ValidationFailure);

public record StreamEvent(
    int Id,
    string Text,
    DateTimeOffset Timestamp);
</code></pre>
<p>A successful request:</p>
<pre><code class="language-http">POST /api/orders
Content-Type: application/json

{&quot;ProductId&quot;:17,&quot;Quantity&quot;:2}
</code></pre>
<p>returns:</p>
<pre><code class="language-json">{&quot;OrderId&quot;:125,&quot;Status&quot;:&quot;created&quot;}
</code></pre>
<p>while an invalid quantity produces the other union shape:</p>
<pre><code class="language-json">{&quot;ErrorCode&quot;:&quot;INVALID_QUANTITY&quot;,&quot;Message&quot;:&quot;Quantity must be greater than zero.&quot;}
</code></pre>
<p>This sample deliberately returns both domain outcomes under HTTP <code>200 OK</code> so that one endpoint can demonstrate a single union response and the generated <code>anyOf</code> schema. That is a serializer/OpenAPI demonstration, not a general HTTP error-handling recommendation. In a production API, validation failures are commonly mapped to an appropriate <code>4xx</code> response.</p>
<hr />
<h2>NDJSON: Streaming JSON Records</h2>
<p>The third major improvement is around asynchronous JSON output.</p>
<p>The read side of this model already existed before .NET 11: <code>JsonSerializer.DeserializeAsyncEnumerable(..., topLevelValues: true)</code> can consume a sequence of whitespace-separated top-level JSON values (available in .NET 9). .NET 11 completes the story on the <strong>write</strong> side by adding top-level-value output to <code>SerializeAsyncEnumerable</code> and direct <code>PipeWriter</code> support.</p>
<p>A conventional JSON array looks like this:</p>
<pre><code class="language-json">[{&quot;Id&quot;:1,&quot;Text&quot;:&quot;Chunk 1&quot;},{&quot;Id&quot;:2,&quot;Text&quot;:&quot;Chunk 2&quot;},{&quot;Id&quot;:3,&quot;Text&quot;:&quot;Chunk 3&quot;}]
</code></pre>
<p>NDJSON — Newline Delimited JSON — looks like this:</p>
<pre><code class="language-text">{&quot;Id&quot;:1,&quot;Text&quot;:&quot;Chunk 1&quot;}
{&quot;Id&quot;:2,&quot;Text&quot;:&quot;Chunk 2&quot;}
{&quot;Id&quot;:3,&quot;Text&quot;:&quot;Chunk 3&quot;}
</code></pre>
<p>Each line is an independent JSON value.</p>
<p>.NET 11 extends <code>JsonSerializer.SerializeAsyncEnumerable</code> with two useful capabilities:</p>
<ul>
<li>writing directly to a <code>PipeWriter</code>,</li>
<li>and a <code>topLevelValues</code> option for NDJSON-style output.</li>
</ul>
<p>The key call from the sample is:</p>
<pre><code class="language-csharp">await JsonSerializer.SerializeAsyncEnumerable(
    context.Response.BodyWriter,
    GenerateEvents(cancellationToken),
    topLevelValues: true,
    options: jsonOptions.Value.SerializerOptions,
    cancellationToken: cancellationToken);
</code></pre>
<p>A .NET client can consume the same record-oriented format with:</p>
<pre><code class="language-csharp">await foreach (var item in JsonSerializer.DeserializeAsyncEnumerable&lt;StreamEvent&gt;(
    responseStream,
    topLevelValues: true))
{
    Console.WriteLine(item);
}
</code></pre>
<p>When relying on line-based framing, verify any serializer formatting changes you make, especially indentation, against the actual wire output expected by your clients.</p>
<p>On Windows, run:</p>
<pre><code class="language-powershell">curl.exe -N http://localhost:5050/api/events/stream
</code></pre>
<p>The wire output is similar to:</p>
<pre><code class="language-text">{&quot;Id&quot;:1,&quot;Text&quot;:&quot;Chunk 1&quot;,&quot;Timestamp&quot;:&quot;...&quot;}
{&quot;Id&quot;:2,&quot;Text&quot;:&quot;Chunk 2&quot;,&quot;Timestamp&quot;:&quot;...&quot;}
{&quot;Id&quot;:3,&quot;Text&quot;:&quot;Chunk 3&quot;,&quot;Timestamp&quot;:&quot;...&quot;}
</code></pre>
<p><img
src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-21-system-text-json-in-net11-naming-policies-union-types-ndjson-streaming/ndjson_screenshot.png"
alt="NDJSON runtime response shown in Scalar"
width="620"
/></p>
<p><em>Figure: Scalar's Try It view shows the runtime response returned with <code>application/x-ndjson</code>. Scalar may pretty-print the values for display; the terminal capture below shows the actual newline-delimited wire format.</em></p>
<p><img
src="https://raw.githubusercontent.com/abpframework/abp/dev/docs/en/Community-Articles/2026-09-21-system-text-json-in-net11-naming-policies-union-types-ndjson-streaming/ndjson_stream.png"
alt="NDJSON records arriving incrementally in Windows Terminal"
width="720"
/></p>
<p><em>Figure: Streaming the NDJSON endpoint with <code>curl.exe -N</code>. Each JSON record arrives independently as it becomes available.</em></p>
<hr />
<h2>JSON Array vs. NDJSON</h2>
<p>A normal JSON array is still the better default when a response is conceptually one document. NDJSON becomes more interesting when individual records are useful before the operation has completely finished.</p>
<p>One important distinction is that <strong>streaming is not unique to NDJSON</strong>. ASP.NET Core has been able to serialize <code>IAsyncEnumerable&lt;T&gt;</code> with <code>System.Text.Json</code> without first buffering the full sequence since .NET 6. The server-side memory benefit comes mainly from streaming the source and avoiding materialization such as <code>ToListAsync()</code>. NDJSON's main difference is its record-oriented framing, which lets clients parse and process complete records incrementally.</p>
<p>| Scenario | JSON array | NDJSON |
|---|---|---|
| Small CRUD or paginated response | Natural fit | Usually unnecessary |
| Client wants <code>response.json()</code> | Natural fit | Poor fit |
| Large streamed result | Can also stream with <code>IAsyncEnumerable&lt;T&gt;</code> | Easy per-record framing and parsing |
| Log/event stream | Possible, but awkward to frame incrementally | Natural fit |
| Incremental search results | Possible | Natural fit |
| ETL/data pipeline | Works | Often convenient |
| Browser AI stream | Possible | Good generic JSON-record format; SSE may be more convenient |
| One atomic document required | Natural fit | Poor fit |</p>
<p>A useful rule of thumb is:</p>
<blockquote>
<p><strong>If the response is conceptually one document, use normal JSON. If it is a sequence of independent records that should be processed as they arrive, NDJSON may be the better model.</strong></p>
</blockquote>
<hr />
<h2>Consuming NDJSON from the Frontend</h2>
<p>There is one important frontend consequence.</p>
<p>This will not work:</p>
<pre><code class="language-ts">const response = await fetch(&quot;/api/events/stream&quot;);
const data = await response.json();
</code></pre>
<p>NDJSON is not one complete JSON document.</p>
<p>Instead, the browser needs to consume the response body as a stream:</p>
<pre><code class="language-ts">type StreamEvent = {
    Id: number;
    Text: string;
    Timestamp: string;
};

async function consumeEvents() {
    const response =
        await fetch(&quot;/api/events/stream&quot;);

    if (!response.ok || !response.body) {
        throw new Error(
            `Streaming request failed: ${response.status}`
        );
    }

    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    let buffer = &quot;&quot;;

    while (true) {
        const { value, done } =
            await reader.read();

        if (done) {
            break;
        }

        buffer += decoder.decode(
            value,
            { stream: true });

        const lines = buffer.split(&quot;\n&quot;);
        buffer = lines.pop() ?? &quot;&quot;;

        for (const line of lines) {
            if (!line.trim()) {
                continue;
            }

            const item =
                JSON.parse(line) as StreamEvent;

            console.log(item);
        }
    }

    buffer += decoder.decode();

    if (buffer.trim()) {
        const item =
            JSON.parse(buffer) as StreamEvent;

        console.log(item);
    }
}
</code></pre>
<p>The buffer is important because network chunks do not necessarily align with NDJSON records.</p>
<p>For example, the browser might receive:</p>
<pre><code class="language-text">{&quot;Id&quot;:1,&quot;Text&quot;:&quot;Chu
</code></pre>
<p>and then:</p>
<pre><code class="language-text">nk 1&quot;}\n{&quot;Id&quot;:2,
</code></pre>
<p>So each <code>reader.read()</code> result cannot safely be passed directly to <code>JSON.parse()</code>.</p>
<hr />
<h2>Large Results and AI Streaming</h2>
<p>Large-result scenarios are where the distinction between <strong>streaming</strong> and <strong>format</strong> becomes especially important.</p>
<p>Consider:</p>
<pre><code class="language-csharp">var rows =
    await db.Transactions
        .ToListAsync();

return Results.Ok(rows);
</code></pre>
<p>If the query returns one million rows, the application materializes the result before serialization.</p>
<p>A streaming pipeline instead looks like this:</p>
<pre><code class="language-text">Database
   │
   ▼
IAsyncEnumerable
   │
   ▼
Serializer
   │
   ▼
Network
</code></pre>
<p>The important point is that the <strong>data source must stream too</strong>. Calling:</p>
<pre><code class="language-csharp">var rows = await query.ToListAsync();
</code></pre>
<p>before writing either a JSON array or NDJSON removes much of the server-side memory benefit.</p>
<p>NDJSON is useful here because the client can parse each complete record independently. A streamed JSON array can also avoid full server-side buffering, but many clients still treat the array as one document and wait for the full payload before calling a normal JSON parser.</p>
<p>There is also an operational trade-off: a truly streamed EF Core query can keep its data reader, database connection, and related upstream resources active while a slow client is still consuming the response. For very large exports, measure this behavior under realistic client speeds and consider whether paging or a background export job is a better fit.</p>
<hr />
<h3>AI Streaming</h3>
<p>AI responses are another natural streaming scenario.</p>
<p>An AI agent may produce more than just text:</p>
<pre><code class="language-text">TextDelta
ToolStarted
ToolResult
Citation
Completed
Error
</code></pre>
<p>An application-level event envelope could look like this:</p>
<pre><code class="language-text">{&quot;type&quot;:&quot;text&quot;,&quot;delta&quot;:&quot;Hello &quot;}
{&quot;type&quot;:&quot;text&quot;,&quot;delta&quot;:&quot;world&quot;}
{&quot;type&quot;:&quot;tool-started&quot;,&quot;name&quot;:&quot;search&quot;}
{&quot;type&quot;:&quot;tool-result&quot;,&quot;count&quot;:4}
{&quot;type&quot;:&quot;completed&quot;,&quot;finishReason&quot;:&quot;stop&quot;}
</code></pre>
<p>The <code>type</code> field above is <strong>application-defined</strong>. C# union serialization does not automatically add a discriminator.</p>
<p>Union types and NDJSON can still complement each other:</p>
<blockquote>
<p><strong>Union types can describe which event shapes are valid in code.</strong></p>
</blockquote>
<blockquote>
<p><strong>NDJSON can describe how independent event records are delivered over time.</strong></p>
</blockquote>
<p>If the JSON contract itself needs an explicit discriminator, model that discriminator deliberately rather than assuming it will appear because the server-side type is a union.</p>
<p>For browser-first server-to-client streaming, SSE may also be worth considering depending on the client requirements.</p>
<hr />
<h2>Error Handling Is Different Once Streaming Starts</h2>
<p>Suppose a server has already sent:</p>
<pre><code class="language-text">HTTP 200 OK

{&quot;Id&quot;:1}
{&quot;Id&quot;:2}
</code></pre>
<p>and then the database fails.</p>
<p>The HTTP response has already started, so the server cannot replace the response with:</p>
<pre><code class="language-http">500 Internal Server Error
</code></pre>
<p>A streaming API therefore needs an explicit strategy for mid-stream errors.</p>
<p>For an event-oriented protocol, that might be:</p>
<pre><code class="language-text">{&quot;type&quot;:&quot;data&quot;,&quot;value&quot;:{&quot;id&quot;:1}}
{&quot;type&quot;:&quot;error&quot;,&quot;code&quot;:&quot;DB_FAILURE&quot;}
</code></pre>
<p>It can also be useful to define a final completion event:</p>
<pre><code class="language-text">{&quot;type&quot;:&quot;data&quot;,&quot;value&quot;:{&quot;id&quot;:1}}
{&quot;type&quot;:&quot;completed&quot;}
</code></pre>
<p>If the connection closes without <code>completed</code>, the client can treat the stream as interrupted rather than successfully finished.</p>
<p>These <code>type</code> fields are part of the application's streaming protocol; they are not inserted automatically by union serialization.</p>
<hr />
<h2>Performance and Cancellation</h2>
<p>NDJSON does not automatically mean “faster”, and it does not automatically use less server memory than a streamed JSON array.</p>
<p>The main benefits of NDJSON are usually:</p>
<ul>
<li>incremental client parsing,</li>
<li>simple record boundaries,</li>
<li>and the ability to process already-completed records before the full response finishes.</li>
</ul>
<p>Memory behavior depends more on whether the producer and serializer stream or buffer the data. Every record still has serialization and parsing overhead, and flushing extremely small records can reduce throughput.</p>
<p>The response may also be buffered by infrastructure such as ASP.NET Core middleware, compression, reverse proxies, CDNs, or the client itself.</p>
<blockquote>
<p><strong>NDJSON defines the format. Streaming, flushing, and buffering define the transport behavior.</strong></p>
</blockquote>
<p>That distinction should be tested in a production-like environment.</p>
<p>Cancellation matters too.</p>
<p>When a browser closes the connection or a user presses <strong>Stop generating</strong>, the request cancellation token should ideally propagate through the entire pipeline:</p>
<pre><code class="language-text">Client disconnect
      │
      ▼
RequestAborted
      │
      ▼
IAsyncEnumerable
      │
      ├── DB query stops
      ├── AI request stops
      └── downstream calls stop
</code></pre>
<p>This can be especially important for AI workloads where unnecessary generation has a real cost.</p>
<hr />
<h2>Compatibility Considerations</h2>
<p>All three features can change a public API contract. Renaming <code>userId</code> to <code>UserId</code> may break generated clients or frontend mappings, and widening a value from one JSON shape to several possible union shapes changes what consumers must handle. Because union serialization is discriminator-free by default, clients may also need shape-based narrowing unless the API defines its own discriminator.</p>
<p>Switching an endpoint from <code>application/json</code> to <code>application/x-ndjson</code> is an even more visible change because clients can no longer rely on <code>response.json()</code> and must consume the body incrementally. For existing APIs, prefer additive changes over silent contract replacements. Keeping <code>GET /api/orders</code> for the conventional JSON response and introducing <code>GET /api/orders/stream</code> for NDJSON, for example, makes the migration explicit and allows existing consumers to continue working unchanged.</p>
<hr />
<h2>RC1 and Production-Readiness Notes</h2>
<p>These features do not all carry the same kind of production risk.</p>
<h3>Naming policies</h3>
<p>The naming APIs are straightforward, but the wire contract is compatibility-sensitive. Treat casing changes exactly like other public-schema changes and contract-test them.</p>
<h3>NDJSON</h3>
<p>NDJSON itself is established, but production behavior depends on the complete delivery path: client parsing, buffering, compression, proxy behavior, cancellation, mid-stream errors, load testing, and observability.</p>
<h3>C# unions</h3>
<p>C# union syntax is <strong>stable in C# 15 as of .NET 11 RC1</strong> and does not require <code>LangVersion=preview</code> for <code>net11.0</code>.</p>
<p>The more important production consideration is deserialization classification. Object-shaped cases can require structural or custom classification. Structural classification has a scanning cost and couples classification to property shape, so contract evolution should be tested carefully.</p>
<p>There is also a documentation inconsistency at the time of writing: the .NET 11 libraries page still contains older wording that calls unions a preview language feature, while the RC1 C# release notes explicitly state that unions were stabilized. For RC1 language status, use the RC1 C# release notes.</p>
<p>.NET 11 RC1 is a Go-Live release, but it is not yet GA. Re-check the final release notes and known issues before the final production rollout.</p>
<hr />
<h2>Running and Testing the Sample</h2>
<p>The screenshots and runtime examples in this article were produced with the locally verified <strong>.NET SDK <code>11.0.100-rc.1.26425.128</code></strong>.</p>
<p>The project uses <code>Microsoft.AspNetCore.OpenApi</code> <code>11.0.0-rc.1.26425.128</code> and <code>Scalar.AspNetCore</code> <code>2.17.4</code>.</p>
<p>To reproduce the setup:</p>
<pre><code class="language-powershell">dotnet new web -n Json11Demo
cd Json11Demo

dotnet add package Microsoft.AspNetCore.OpenApi --version 11.0.0-rc.1.26425.128
dotnet add package Scalar.AspNetCore --version 2.17.4
</code></pre>
<p>Replace the generated project file and <code>Program.cs</code> with the versions shown above, then verify the installed SDK and build the project:</p>
<pre><code class="language-powershell">dotnet --version
dotnet build
dotnet run --urls http://localhost:5050
</code></pre>
<p>The local <code>dotnet --version</code> output was <code>11.0.100-rc.1.26425.128</code>, matching the SDK build listed in the .NET 11 RC1 release notes. The project built successfully and the API was then started on <code>http://localhost:5050</code>.</p>
<p>The successful order request used during testing was:</p>
<pre><code class="language-powershell">Invoke-RestMethod `
  -Uri &quot;http://localhost:5050/api/orders&quot; `
  -Method Post `
  -ContentType &quot;application/json&quot; `
  -Body '{&quot;ProductId&quot;:17,&quot;Quantity&quot;:2}'
</code></pre>
<p>PowerShell displayed:</p>
<pre><code class="language-text">OrderId Status
------- ------
125     created
</code></pre>
<p>The validation branch was also tested with <code>Quantity</code> set to <code>0</code>:</p>
<pre><code class="language-powershell">Invoke-RestMethod `
  -Uri &quot;http://localhost:5050/api/orders&quot; `
  -Method Post `
  -ContentType &quot;application/json&quot; `
  -Body '{&quot;ProductId&quot;:17,&quot;Quantity&quot;:0}'
</code></pre>
<p>PowerShell displayed:</p>
<pre><code class="language-text">ErrorCode        Message
---------        -------
INVALID_QUANTITY Quantity must be greater than zero.
</code></pre>
<p>The NDJSON endpoint was tested with:</p>
<pre><code class="language-powershell">curl.exe -N http://localhost:5050/api/events/stream
</code></pre>
<p>and produced records incrementally:</p>
<pre><code class="language-text">{&quot;Id&quot;:1,&quot;Text&quot;:&quot;Chunk 1&quot;,&quot;Timestamp&quot;:&quot;...&quot;}
{&quot;Id&quot;:2,&quot;Text&quot;:&quot;Chunk 2&quot;,&quot;Timestamp&quot;:&quot;...&quot;}
{&quot;Id&quot;:3,&quot;Text&quot;:&quot;Chunk 3&quot;,&quot;Timestamp&quot;:&quot;...&quot;}
</code></pre>
<p>The generated OpenAPI document was also inspected in Scalar at:</p>
<pre><code class="language-text">http://localhost:5050/scalar/v1
</code></pre>
<p>where <code>CreateOrderResult</code> appeared as an <code>anyOf</code> choice between <code>OrderCreated</code> and <code>ValidationFailure</code>.</p>
<p>A separate <code>net11.0</code> console check was used to verify union deserialization. The same JSON object was deserialized first into an object-object union without a classifier, and then into the same union shape with <code>JsonUnionTypeStructuralClassifier</code>.</p>
<p>Input:</p>
<pre><code class="language-json">{&quot;OrderId&quot;:125,&quot;Status&quot;:&quot;created&quot;}
</code></pre>
<p>Observed output:</p>
<pre><code class="language-text">Default: JsonException: JSON value type 'Object' is ambiguous for union type 'DefaultResult' because multiple case types can use this value type. Specify a custom type classifier to support deserialization. Path: $ | LineNumber: 0 | BytePositionInLine: 1.
Structural: OrderCreated
</code></pre>
<p>This confirms the distinction discussed earlier: serialization of the active case is straightforward, but deserializing two object-shaped cases requires classification. For these structurally distinct cases, <code>JsonUnionTypeStructuralClassifier</code> successfully selected <code>OrderCreated</code>.</p>
<hr />
<h2>Final Thoughts</h2>
<p>The most useful way to look at these <code>System.Text.Json</code> improvements is not as three unrelated serializer features.</p>
<p>They affect three different parts of an API contract:</p>
<pre><code class="language-text">Naming policy → name
Union type    → shape
NDJSON        → delivery
</code></pre>
<p>For normal CRUD and paginated endpoints, conventional JSON remains the simpler choice.</p>
<p>For large or long-running responses, the first architectural decision is whether the source and serializer should stream at all. NDJSON then becomes useful when the client benefits from independent, line-delimited records that can be parsed as they arrive.</p>
<p>Union types make fixed alternative shapes explicit in C#, while the naming-policy improvements provide more control over compatibility-sensitive JSON property names. The important production caveat is that serialization and deserialization are not the same problem: writing the active union case is simple, while reading ambiguous shapes may require classification.</p>
<p>Together, these .NET 11 changes give API developers more explicit control over how contracts are named, shaped, and delivered.</p>
<hr />
<h2>Adoption Checklist</h2>
<p>Before adopting these features:</p>
<ul>
<li>[ ] Treat JSON property casing as part of the public contract.</li>
<li>[ ] Contract-test naming-policy changes and per-member overrides.</li>
<li>[ ] Keep frontend types aligned with the actual wire casing.</li>
<li>[ ] Inspect generated OpenAPI <code>anyOf</code> schemas for union responses.</li>
<li>[ ] Test union deserialization separately from serialization.</li>
<li>[ ] Add structural or custom classification when union cases are ambiguous.</li>
<li>[ ] Consider a discriminator-based closed hierarchy for new related object contracts you fully control.</li>
<li>[ ] Use NDJSON when independent records should be processed incrementally.</li>
<li>[ ] Remember that streamed JSON arrays can also avoid full server-side buffering.</li>
<li>[ ] Make sure the underlying data source streams instead of materializing with <code>ToListAsync()</code>.</li>
<li>[ ] Test slow-client behavior and upstream resource lifetime for large streamed queries.</li>
<li>[ ] Use an incremental frontend parser instead of <code>response.json()</code> for NDJSON.</li>
<li>[ ] Handle records that span multiple network chunks and flush any final decoder buffer.</li>
<li>[ ] Propagate request cancellation to downstream work.</li>
<li>[ ] Define mid-stream error and completion semantics.</li>
<li>[ ] Test proxy, compression, flushing, and buffering behavior.</li>
<li>[ ] Compare NDJSON with SSE for browser-focused AI streaming.</li>
<li>[ ] Re-check .NET 11 release notes and known issues before moving from RC to GA.</li>
</ul>
<h2>References</h2>
<ul>
<li><a href="https://github.com/dotnet/core/blob/main/release-notes/11.0/preview/rc1/11.0.0-rc.1.md">.NET 11.0.0 RC1 release notes</a></li>
<li><a href="https://github.com/dotnet/core/blob/main/release-notes/11.0/preview/rc1/csharp.md">C# in .NET 11 RC1 release notes</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-11/libraries">What's new in .NET libraries for .NET 11</a></li>
<li><a href="https://devblogs.microsoft.com/dotnet/unions-and-closed-hierarchies-in-aspnetcore/">Use C# unions and closed hierarchies in ASP.NET Core</a></li>
<li><a href="https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview">System.Text.Json overview</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi">Generate OpenAPI documents in ASP.NET Core</a></li>
<li><a href="https://learn.microsoft.com/en-us/aspnet/core/breaking-changes/6/iasyncenumerable-not-buffered-by-mvc">MVC doesn't buffer IAsyncEnumerable types when using System.Text.Json</a></li>
<li><a href="https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying">Efficient querying in EF Core — buffering and streaming</a></li>
</ul>
]]></content:encoded>
      <media:thumbnail url="https://abp.io/api/posts/cover-picture-source/3a23d62f-8594-0942-7648-2c3d8fc44d04" />
      <media:content url="https://abp.io/api/posts/cover-picture-source/3a23d62f-8594-0942-7648-2c3d8fc44d04" medium="image" />
    </item>
  </channel>
</rss>