{"id":2291,"date":"2026-04-08T09:12:53","date_gmt":"2026-04-08T09:12:53","guid":{"rendered":"https:\/\/www.w3computing.com\/articles\/?p=2291"},"modified":"2026-04-08T09:32:05","modified_gmt":"2026-04-08T09:32:05","slug":"zero-allocation-serialization-with-system-text-json-source-generators","status":"publish","type":"post","link":"https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/","title":{"rendered":"Zero-Allocation Serialization with System.Text.Json Source Generators"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">1. Real-World Problem Scenario<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A few months ago, we were tuning a high-throughput .NET API responsible for ingesting pricing events\u2014nothing exotic, just a stream of relatively small DTOs (~200\u2013400 bytes each). The service itself was straightforward: deserialize request \u2192 process \u2192 serialize response. On paper, this shouldn\u2019t have been the bottleneck.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But under sustained load (~40\u201360k requests\/sec), things started to fall apart.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CPU was high, but not alarmingly so. The real issue showed up in <strong>GC metrics<\/strong>\u2014Gen 0 collections were firing constantly, and under burst traffic, we\u2019d occasionally hit Gen 2 pauses that introduced visible latency spikes (P99 went from ~12ms to ~80ms). That\u2019s where things got interesting.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We profiled the hot path and found something unintuitive: serialization was one of the top allocation contributors.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The code looked harmless:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">var<\/span> json = JsonSerializer.Serialize(order);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This is the kind of line nobody questions in code review. But under the hood, this triggers a reflection-driven pipeline:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">Order \u2192 Reflection metadata lookup \u2192 Property inspection \u2192 JsonTypeInfo creation \u2192 Serialization<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">plaintext<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">plaintext<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Even though <code>System.Text.Json<\/code> caches metadata, the <strong>initial graph construction, boxing, and intermediate allocations<\/strong> still add up\u2014especially when multiplied by tens of thousands of requests per second.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We tried the usual fixes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Reusing <code>JsonSerializerOptions<\/code><\/li>\n\n\n\n<li>Reducing object churn elsewhere<\/li>\n\n\n\n<li>Even benchmarking alternative libraries<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Nothing meaningfully moved the needle.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The key realization came from allocation profiling\u2014not CPU profiling. The problem wasn\u2019t that serialization was <em>slow<\/em>, it was that it was <strong>allocation-heavy<\/strong>, which indirectly killed throughput via GC pressure.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In other words, we weren\u2019t CPU-bound\u2014we were <strong>memory churn bound<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That\u2019s the moment where runtime-based serialization starts to show its limits. If the bottleneck is allocations, the only real fix is to <strong>eliminate the runtime work entirely<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Which leads us to the core idea: move serialization metadata generation from runtime \u2192 compile time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Understanding the Core Concept<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When people hear \u201czero-allocation serialization,\u201d it\u2019s easy to misinterpret it as some kind of magic where <em>nothing<\/em> gets allocated. That\u2019s not the goal\u2014and in practice, it\u2019s not even realistic. What we\u2019re actually doing is eliminating <strong>avoidable allocations in the serialization pipeline<\/strong>, particularly those caused by runtime type inspection.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The default <code>System.Text.Json<\/code> pipeline is optimized, but it\u2019s still fundamentally <strong>reflection-driven<\/strong>. At some point, it has to walk your type graph, inspect properties, build metadata (<code>JsonTypeInfo<\/code>), and cache it. That work isn\u2019t free.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With source generators, we shift that entire process to compile time.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Instead of this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">Runtime:\nDTO \u2192 Reflection \u2192 Build metadata \u2192 Cache \u2192 Serialize<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">plaintext<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">plaintext<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">We get this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"plaintext\" data-shcb-language-slug=\"plaintext\"><span><code class=\"hljs language-plaintext\">Compile-time:\nDTO \u2192 Generate metadata + serializers\n\nRuntime:\nDTO \u2192 Use prebuilt metadata \u2192 Serialize directly<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">plaintext<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">plaintext<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">That shift is where the real gains come from.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At build time, a Roslyn-based source generator scans your DTOs and produces strongly-typed serialization code. This includes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Precomputed property mappings<\/li>\n\n\n\n<li>Optimized accessors (no reflection, no boxing)<\/li>\n\n\n\n<li>Prebuilt <code>JsonTypeInfo&lt;T><\/code> graphs<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">So when you call the serializer at runtime, there\u2019s no discovery phase\u2014it already knows exactly how to serialize your type.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Concretely, instead of relying on:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\">JsonSerializer.Serialize(order);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">You use a generated context:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\">JsonSerializer.Serialize(order, OrderJsonContext.Default.Order);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">That second argument is the key\u2014it tells the serializer: <em>\u201cDon\u2019t figure this out. Use this precompiled contract.\u201d<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Another important detail: this integrates tightly with <code>Utf8JsonWriter<\/code>, which writes directly to buffers using <code>Span&lt;byte&gt;<\/code>. That means you\u2019re not just avoiding reflection\u2014you\u2019re also avoiding unnecessary intermediate representations (like strings).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Where this really starts to matter:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>High-throughput APIs<\/li>\n\n\n\n<li>Low-latency services<\/li>\n\n\n\n<li>Serverless cold starts<\/li>\n\n\n\n<li>Native AOT builds (where reflection can break entirely)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">At a high level, you can think of source generators as turning JSON serialization from a <strong>dynamic runtime system<\/strong> into a <strong>static, predictable pipeline<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">And once you remove that dynamism, you unlock both performance and control\u2014which we\u2019ll dissect in detail next.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3. Internal Mechanics \/ How It Works<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">To understand why source generation helps so much, you have to stop thinking about <code>JsonSerializer<\/code> as a single method call and start thinking about it as a <strong>metadata pipeline<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In the default path, this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">var<\/span> json = JsonSerializer.Serialize(order);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">is really shorthand for: determine the runtime type, inspect its shape, build or retrieve serialization metadata, resolve converters, and only then write JSON. Much of that gets cached, but the first-hit cost is real, and in large services with many DTOs, \u201cfirst hit\u201d happens more often than people think.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A simplified version of the runtime path looks like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-keyword\">string<\/span> <span class=\"hljs-title\">SerializeOrder<\/span>(<span class=\"hljs-params\">Order order, JsonSerializerOptions options<\/span>)<\/span>\n{\n    JsonTypeInfo typeInfo = options.GetTypeInfo(<span class=\"hljs-keyword\">typeof<\/span>(Order)); <span class=\"hljs-comment\">\/\/ May trigger metadata creation<\/span>\n    <span class=\"hljs-keyword\">return<\/span> JsonSerializer.Serialize(order, typeInfo);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">That <code>GetTypeInfo()<\/code> step is where the serializer may walk the type graph, inspect properties, evaluate attributes, resolve naming policies, and wire up converters. It is efficient by general-purpose-library standards, but it is still <strong>dynamic infrastructure<\/strong>, and dynamic infrastructure allocates.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Source generation removes that discovery phase by emitting a strongly typed context at build time:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">using<\/span> System.Text.Json.Serialization;\n\n&#91;<span class=\"hljs-meta\">JsonSourceGenerationOptions(\n    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,\n    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(Order))<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(OrderLine))<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(List&lt;Order&gt;))<\/span>]\n<span class=\"hljs-keyword\">internal<\/span> <span class=\"hljs-keyword\">partial<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">OrderJsonContext<\/span> : <span class=\"hljs-title\">JsonSerializerContext<\/span>\n{\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This is not just configuration sugar. In production, this is how you make the serializer contract <strong>explicit<\/strong>. The generator produces <code>JsonTypeInfo&lt;Order&gt;<\/code>, <code>JsonTypeInfo&lt;OrderLine&gt;<\/code>, and the supporting metadata graph up front, so runtime no longer needs to discover the shape of those objects.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then you consume that prebuilt metadata directly:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">var<\/span> json = JsonSerializer.Serialize(order, OrderJsonContext.Default.Order);\n<span class=\"hljs-keyword\">var<\/span> ordersJson = JsonSerializer.Serialize(orders, OrderJsonContext.Default.ListOrder);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The trade-off is obvious and worth stating plainly: you lose some of the convenience of \u201cserialize anything\u201d APIs in exchange for predictability. In small apps, that may feel like boilerplate. In hot paths, it is exactly the kind of boilerplate you want, because it makes reflection fallback much harder to introduce by accident.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The next layer is where the allocation story gets better: writing directly to UTF-8 buffers instead of materializing JSON strings.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">using<\/span> System.Buffers;\n<span class=\"hljs-keyword\">using<\/span> System.Text.Json;\n\n<span class=\"hljs-keyword\">var<\/span> buffer = <span class=\"hljs-keyword\">new<\/span> ArrayBufferWriter&lt;<span class=\"hljs-keyword\">byte<\/span>&gt;(<span class=\"hljs-number\">256<\/span>);\n\n<span class=\"hljs-keyword\">using<\/span> (<span class=\"hljs-keyword\">var<\/span> writer = <span class=\"hljs-keyword\">new<\/span> Utf8JsonWriter(buffer))\n{\n    JsonSerializer.Serialize(writer, order, OrderJsonContext.Default.Order);\n}\n\nReadOnlySpan&lt;<span class=\"hljs-keyword\">byte<\/span>&gt; payload = buffer.WrittenSpan;\n<span class=\"hljs-comment\">\/\/ Send payload to network stream, pipe, or response body<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This pattern matters in production for two reasons.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First, <code>string<\/code> is often the wrong output type for service-to-service communication. If the next step is an HTTP response body, a pipe, Kafka producer, or socket write, creating a UTF-16 <code>string<\/code> only to re-encode it to UTF-8 is wasted work.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Second, <code>Utf8JsonWriter<\/code> gives you tighter control over buffering. Combined with pooled buffers or response streams, it lets you keep the hot path closer to raw bytes and farther away from avoidable heap churn.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The main trade-off here is complexity. A direct-to-buffer pipeline is less ergonomic than <code>Serialize(obj)<\/code>, and you need to manage lifetimes and buffer growth carefully. But that is exactly why high-throughput services use it: fewer abstractions in the hot path usually means fewer surprises under load.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So the production pattern is not just \u201cuse source generators.\u201d It is:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\">DTO contract known at compile time\n        \u2193\nGenerated JsonTypeInfo&lt;T&gt;\n        \u2193\nUtf8JsonWriter writes UTF<span class=\"hljs-number\">-8<\/span> directly\n        \u2193\nFewer allocations, less GC pressure, more predictable latency<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">That predictability is the real win. Once serialization stops doing runtime discovery, performance becomes much easier to reason about\u2014which makes the implementation phase much more deliberate.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4. Step-by-Step Implementation<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The easiest way to get source generation wrong is to treat it like a cosmetic optimization: add a context class, keep the rest of the code the same, and assume the runtime will somehow do the right thing. In production, that usually leads to a half-migrated setup where some DTOs use generated metadata and others quietly fall back to reflection.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The implementation needs to be deliberate.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4.1 Start with the baseline<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A typical service begins here:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">using<\/span> System.Text.Json;\n\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">OrderResponseWriter<\/span>\n{\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-keyword\">string<\/span> <span class=\"hljs-title\">Write<\/span>(<span class=\"hljs-params\">OrderResponse response<\/span>)<\/span>\n    {\n        <span class=\"hljs-keyword\">return<\/span> JsonSerializer.Serialize(response);\n    }\n}\n\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">sealed<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">OrderResponse<\/span>\n{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">string<\/span> OrderId { <span class=\"hljs-keyword\">get<\/span>; init; } = <span class=\"hljs-keyword\">default<\/span>!;\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">decimal<\/span> Total { <span class=\"hljs-keyword\">get<\/span>; init; }\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">string<\/span> Currency { <span class=\"hljs-keyword\">get<\/span>; init; } = <span class=\"hljs-keyword\">default<\/span>!;\n    <span class=\"hljs-keyword\">public<\/span> List&lt;OrderLine&gt; Lines { <span class=\"hljs-keyword\">get<\/span>; init; } = <span class=\"hljs-keyword\">new<\/span>();\n}\n\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">sealed<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">OrderLine<\/span>\n{\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">string<\/span> Sku { <span class=\"hljs-keyword\">get<\/span>; init; } = <span class=\"hljs-keyword\">default<\/span>!;\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">int<\/span> Quantity { <span class=\"hljs-keyword\">get<\/span>; init; }\n    <span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">decimal<\/span> UnitPrice { <span class=\"hljs-keyword\">get<\/span>; init; }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This is fine for internal tools, low-volume endpoints, and prototypes. The problem is that the serialization contract is implicit. The runtime has to infer everything from the CLR type, which means metadata discovery, converter resolution, and cache population all happen dynamically.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That dynamic behavior is exactly what makes the API convenient\u2014and exactly what makes it harder to reason about in hot paths.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4.2 Make the serialization contract explicit<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The first real step is defining a context that lists every type you intend to serialize on the hot path.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">using<\/span> System.Text.Json.Serialization;\n\n&#91;<span class=\"hljs-meta\">JsonSourceGenerationOptions(\n    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,\n    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,\n    GenerationMode = JsonSourceGenerationMode.Serialization)<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(OrderResponse))<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(OrderLine))<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(List&lt;OrderLine&gt;))<\/span>]\n<span class=\"hljs-keyword\">internal<\/span> <span class=\"hljs-keyword\">partial<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">OrderJsonContext<\/span> : <span class=\"hljs-title\">JsonSerializerContext<\/span>\n{\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">A few important things are happening here.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>[JsonSerializable]<\/code> is the contract declaration. It tells the generator which types must have metadata generated at build time. If you forget a type, that type is not part of the generated graph, and you risk fallback behavior later.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>GenerationMode = JsonSourceGenerationMode.Serialization<\/code> is also worth calling out. If your service only writes JSON and never reads it back, there is no reason to generate more machinery than necessary. In production systems, being explicit about scope matters. Smaller generated output means less code, less noise, and slightly less build-time overhead.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">At this point, the generated code is build output, not something you hand-maintain. That is exactly why this is production-friendly: the serializer contract becomes reviewable and intentional without forcing you to manually write serializers.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4.3 Use the generated overloads, not the convenient ones<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is the part teams often miss. Defining a context does nothing unless you actually call the serializer with it.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">using<\/span> System.Text.Json;\n\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">OrderResponseWriter<\/span>\n{\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-keyword\">string<\/span> <span class=\"hljs-title\">Write<\/span>(<span class=\"hljs-params\">OrderResponse response<\/span>)<\/span>\n    {\n        <span class=\"hljs-keyword\">return<\/span> JsonSerializer.Serialize(\n            response,\n            OrderJsonContext.Default.OrderResponse);\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This overload matters because it bypasses runtime type discovery and uses the prebuilt <code>JsonTypeInfo&lt;OrderResponse&gt;<\/code> directly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That is not just a stylistic preference. In code review, this is the difference between:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u201cwe configured source generation\u201d<\/li>\n\n\n\n<li>and<\/li>\n\n\n\n<li>\u201cwe are definitely using source generation on this path\u201d<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The trade-off is verbosity. <code>JsonSerializer.Serialize(response)<\/code> is cleaner to look at. But production performance work is full of cases where the shorter API is not the better API. Here, the explicit overload buys you correctness and predictability.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4.4 Stop allocating strings when you only need bytes<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In real services, JSON usually does not end its life as a <code>string<\/code>. It gets written to an HTTP response, a broker, a stream, or a pipeline. Creating a string first often means:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>build UTF-16 text in memory<\/li>\n\n\n\n<li>re-encode it to UTF-8<\/li>\n\n\n\n<li>write the bytes somewhere else<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">That round trip is unnecessary.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A more realistic production path writes directly to a byte buffer:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">using<\/span> System.Buffers;\n<span class=\"hljs-keyword\">using<\/span> System.Text.Json;\n\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">OrderResponseBufferWriter<\/span>\n{\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">static<\/span> ReadOnlyMemory&lt;<span class=\"hljs-keyword\">byte<\/span>&gt; <span class=\"hljs-title\">Write<\/span>(<span class=\"hljs-params\">OrderResponse response<\/span>)<\/span>\n    {\n        <span class=\"hljs-keyword\">var<\/span> buffer = <span class=\"hljs-keyword\">new<\/span> ArrayBufferWriter&lt;<span class=\"hljs-keyword\">byte<\/span>&gt;(<span class=\"hljs-number\">512<\/span>);\n\n        <span class=\"hljs-keyword\">using<\/span> (<span class=\"hljs-keyword\">var<\/span> jsonWriter = <span class=\"hljs-keyword\">new<\/span> Utf8JsonWriter(buffer))\n        {\n            JsonSerializer.Serialize(\n                jsonWriter,\n                response,\n                OrderJsonContext.Default.OrderResponse);\n        }\n\n        <span class=\"hljs-keyword\">return<\/span> buffer.WrittenMemory;\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This pattern is common in services that care about throughput because it keeps the pipeline in UTF-8 from start to finish. No intermediate string, no second encoding step, and much better alignment with how ASP.NET Core, pipelines, and network APIs already operate.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The trade-off is lifecycle management. Returning <code>ReadOnlyMemory&lt;byte&gt;<\/code> is safe here because <code>ArrayBufferWriter&lt;byte&gt;<\/code> owns the memory for the lifetime of the returned object, but if you start pooling buffers aggressively, ownership gets trickier. This is exactly why \u201czero-allocation\u201d code needs discipline: once you optimize the hot path, memory lifetime becomes part of your design.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4.5 Write straight to a stream in server code<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If the destination is already a stream, skip the intermediate buffer too.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">using<\/span> System.IO;\n<span class=\"hljs-keyword\">using<\/span> System.Text.Json;\n<span class=\"hljs-keyword\">using<\/span> System.Threading;\n<span class=\"hljs-keyword\">using<\/span> System.Threading.Tasks;\n\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">OrderResponseStreamWriter<\/span>\n{\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">static<\/span> <span class=\"hljs-keyword\">async<\/span> Task <span class=\"hljs-title\">WriteAsync<\/span>(<span class=\"hljs-params\">\n        Stream output,\n        OrderResponse response,\n        CancellationToken cancellationToken = <span class=\"hljs-keyword\">default<\/span><\/span>)<\/span>\n    {\n        <span class=\"hljs-keyword\">await<\/span> JsonSerializer.SerializeAsync(\n            output,\n            response,\n            OrderJsonContext.Default.OrderResponse,\n            cancellationToken);\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This is the pattern you want in HTTP handlers, background exporters, or file-based batch jobs. It minimizes intermediate allocations and lets the runtime write bytes directly to the output stream.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Why this is used in production:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>fewer copies<\/li>\n\n\n\n<li>better memory behavior under concurrency<\/li>\n\n\n\n<li>simpler integration with response bodies and file streams<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The trade-off is that once you stream directly, you lose some flexibility for post-processing. If you need to inspect, mutate, or log the full payload before sending it, buffering may still be useful. That is a design choice, not a universal rule.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4.6 Integrate it properly in ASP.NET Core<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If your service is ASP.NET Core-based, the cleanest way is to register the generated resolver globally so framework serialization also uses it.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">using<\/span> Microsoft.AspNetCore.Builder;\n<span class=\"hljs-keyword\">using<\/span> Microsoft.Extensions.DependencyInjection;\n\n<span class=\"hljs-keyword\">var<\/span> builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.ConfigureHttpJsonOptions(options =&gt;\n{\n    options.SerializerOptions.TypeInfoResolverChain.Insert(<span class=\"hljs-number\">0<\/span>, OrderJsonContext.Default);\n});\n\n<span class=\"hljs-keyword\">var<\/span> app = builder.Build();\n\napp.MapGet(<span class=\"hljs-string\">\"\/orders\/{id}\"<\/span>, (<span class=\"hljs-keyword\">string<\/span> id) =&gt;\n{\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> OrderResponse\n    {\n        OrderId = id,\n        Total = <span class=\"hljs-number\">149.95<\/span>m,\n        Currency = <span class=\"hljs-string\">\"USD\"<\/span>,\n        Lines =\n        {\n            <span class=\"hljs-keyword\">new<\/span> OrderLine { Sku = <span class=\"hljs-string\">\"KB-001\"<\/span>, Quantity = <span class=\"hljs-number\">1<\/span>, UnitPrice = <span class=\"hljs-number\">99.95<\/span>m },\n            <span class=\"hljs-keyword\">new<\/span> OrderLine { Sku = <span class=\"hljs-string\">\"MS-002\"<\/span>, Quantity = <span class=\"hljs-number\">1<\/span>, UnitPrice = <span class=\"hljs-number\">50.00<\/span>m }\n        }\n    };\n});\n\napp.Run();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This is important because it removes one of the most common production mistakes: manually using source generation in one helper method while MVC\/minimal APIs continue serializing other responses through the default reflection-driven configuration.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">By inserting your context at the start of the resolver chain, you make the optimized path the first choice. That does not magically cover every type in the application, but it makes your intent explicit and reduces accidental regressions.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The trade-off is governance. Once you centralize the resolver, you need a process for keeping the context updated as new DTOs are introduced. In a fast-moving codebase, a stale context is a real maintenance risk. The upside is that this risk is visible. Reflection fallback is invisible until load testing or production tells you otherwise.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4.7 Cover collections and nested DTOs intentionally<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Another easy mistake is assuming that declaring the root type is enough. Sometimes it is, sometimes it is not, and relying on \u201cprobably\u201d is how performance regressions sneak in.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Be explicit with frequently used shapes:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\">&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(OrderResponse))<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(OrderLine))<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(List&lt;OrderLine&gt;))<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(Dictionary&lt;string, decimal&gt;))<\/span>]\n<span class=\"hljs-keyword\">internal<\/span> <span class=\"hljs-keyword\">partial<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">OrderJsonContext<\/span> : <span class=\"hljs-title\">JsonSerializerContext<\/span>\n{\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Why this matters in production:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>list-heavy payloads are common<\/li>\n\n\n\n<li>dictionary-based extension fields are common<\/li>\n\n\n\n<li>nested DTOs often dominate real response shapes<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The performance gain is not just raw speed. It is the removal of uncertainty. When a service starts pushing large nested payloads, you do not want to wonder whether a rarely used shape is going through a dynamic path.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4.8 Verify the migration instead of assuming it worked<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A production rollout should always verify generated usage. One practical step is enabling generated file emission during development:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">PropertyGroup<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">EmitCompilerGeneratedFiles<\/span>&gt;<\/span>true<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">EmitCompilerGeneratedFiles<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">CompilerGeneratedFilesOutputPath<\/span>&gt;<\/span>Generated<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">CompilerGeneratedFilesOutputPath<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">PropertyGroup<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">That gives you a way to inspect what was actually produced. You do not need to read every generated file line-by-line, but you should confirm:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>the expected types exist<\/li>\n\n\n\n<li>the context is being generated<\/li>\n\n\n\n<li>you are not relying on wishful thinking<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">That last point matters more than it sounds. Source generation is powerful, but it is not self-enforcing. The production-grade version of this pattern is not \u201cadd attributes and hope.\u201d It is:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>define the contract explicitly<\/li>\n\n\n\n<li>use the generated overloads explicitly<\/li>\n\n\n\n<li>write to bytes or streams when the hot path justifies it<\/li>\n\n\n\n<li>register the context centrally where frameworks serialize on your behalf<\/li>\n\n\n\n<li>verify the result<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Once those pieces are in place, the serializer stops being a black box and becomes part of your performance architecture\u2014which is exactly where it should be before we move into a real-world case study.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5. Real-World Implementation Case Study<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Let\u2019s return to the kind of system where this actually matters: an event ingestion API that receives pricing updates, enriches them, and republishes them downstream. The service was not doing anything computationally expensive. The pressure came from <strong>volume<\/strong>. Tens of thousands of small JSON payloads per second meant serialization overhead was no longer background noise\u2014it was part of the core latency budget.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The original write path looked like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">sealed<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">PricingEventPublisher<\/span>\n{\n    <span class=\"hljs-keyword\">private<\/span> <span class=\"hljs-keyword\">readonly<\/span> HttpClient _httpClient;\n\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-title\">PricingEventPublisher<\/span>(<span class=\"hljs-params\">HttpClient httpClient<\/span>)<\/span>\n    {\n        _httpClient = httpClient;\n    }\n\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">async<\/span> Task <span class=\"hljs-title\">PublishAsync<\/span>(<span class=\"hljs-params\">PricingEvent pricingEvent, CancellationToken cancellationToken<\/span>)<\/span>\n    {\n        <span class=\"hljs-keyword\">var<\/span> json = JsonSerializer.Serialize(pricingEvent);\n\n        <span class=\"hljs-keyword\">using<\/span> <span class=\"hljs-keyword\">var<\/span> content = <span class=\"hljs-keyword\">new<\/span> StringContent(json, Encoding.UTF8, <span class=\"hljs-string\">\"application\/json\"<\/span>);\n        <span class=\"hljs-keyword\">await<\/span> _httpClient.PostAsync(<span class=\"hljs-string\">\"\/events\/pricing\"<\/span>, content, cancellationToken);\n    }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This is normal application code. It is also doing more work than it appears:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>allocate JSON as a <code>string<\/code><\/li>\n\n\n\n<li>encode that string back into UTF-8<\/li>\n\n\n\n<li>allocate <code>StringContent<\/code><\/li>\n\n\n\n<li>pay runtime serializer metadata costs<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Under burst load, that combination produced steady GC churn. Nothing catastrophic in isolation, but enough to drag tail latency upward.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The migrated version moved the path to generated metadata plus byte-oriented output:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\">&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(PricingEvent))<\/span>]\n<span class=\"hljs-keyword\">internal<\/span> <span class=\"hljs-keyword\">partial<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">PricingJsonContext<\/span> : <span class=\"hljs-title\">JsonSerializerContext<\/span>\n{\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-23\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">sealed<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">PricingEventPublisher<\/span>\n{\n    <span class=\"hljs-keyword\">private<\/span> <span class=\"hljs-keyword\">readonly<\/span> HttpClient _httpClient;\n\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-title\">PricingEventPublisher<\/span>(<span class=\"hljs-params\">HttpClient httpClient<\/span>)<\/span>\n    {\n        _httpClient = httpClient;\n    }\n\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">async<\/span> Task <span class=\"hljs-title\">PublishAsync<\/span>(<span class=\"hljs-params\">PricingEvent pricingEvent, CancellationToken cancellationToken<\/span>)<\/span>\n    {\n        <span class=\"hljs-keyword\">var<\/span> buffer = <span class=\"hljs-keyword\">new<\/span> ArrayBufferWriter&lt;<span class=\"hljs-keyword\">byte<\/span>&gt;(<span class=\"hljs-number\">256<\/span>);\n\n        <span class=\"hljs-keyword\">using<\/span> (<span class=\"hljs-keyword\">var<\/span> writer = <span class=\"hljs-keyword\">new<\/span> Utf8JsonWriter(buffer))\n        {\n            JsonSerializer.Serialize(\n                writer,\n                pricingEvent,\n                PricingJsonContext.Default.PricingEvent);\n        }\n\n        <span class=\"hljs-keyword\">using<\/span> <span class=\"hljs-keyword\">var<\/span> content = <span class=\"hljs-keyword\">new<\/span> ByteArrayContent(buffer.WrittenMemory.ToArray());\n        content.Headers.ContentType = <span class=\"hljs-keyword\">new<\/span> System.Net.Http.Headers.MediaTypeHeaderValue(<span class=\"hljs-string\">\"application\/json\"<\/span>);\n\n        <span class=\"hljs-keyword\">await<\/span> _httpClient.PostAsync(<span class=\"hljs-string\">\"\/events\/pricing\"<\/span>, content, cancellationToken);\n    }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This was not \u201cperfect zero-allocation\u201d because <code>ToArray()<\/code> still copies. But it removed the expensive parts first: string creation, re-encoding, and runtime metadata discovery. That is a common production trade-off\u2014take the biggest wins without making the codebase brittle.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In the final version, we pushed one step further and wrote directly to a stream-backed content path:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-24\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">using<\/span> <span class=\"hljs-keyword\">var<\/span> stream = <span class=\"hljs-keyword\">new<\/span> MemoryStream();\n<span class=\"hljs-keyword\">await<\/span> JsonSerializer.SerializeAsync(\n    stream,\n    pricingEvent,\n    PricingJsonContext.Default.PricingEvent,\n    cancellationToken);\n\nstream.Position = <span class=\"hljs-number\">0<\/span>;\n<span class=\"hljs-keyword\">using<\/span> <span class=\"hljs-keyword\">var<\/span> content = <span class=\"hljs-keyword\">new<\/span> StreamContent(stream);\ncontent.Headers.ContentType = <span class=\"hljs-keyword\">new<\/span> System.Net.Http.Headers.MediaTypeHeaderValue(<span class=\"hljs-string\">\"application\/json\"<\/span>);\n<span class=\"hljs-keyword\">await<\/span> _httpClient.PostAsync(<span class=\"hljs-string\">\"\/events\/pricing\"<\/span>, content, cancellationToken);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-24\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The result was exactly what you want from a production optimization: lower allocation rate, smoother GC behavior, and a measurable drop in P99 latency\u2014without changing the business logic at all.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">6. Performance Analysis and Optimization<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">At this point, the question is no longer \u201cdoes source generation sound faster?\u201d but \u201cwhere is the gain actually coming from?\u201d The only useful answer is measurement.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A minimal BenchmarkDotNet setup makes the difference visible:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-25\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">using<\/span> BenchmarkDotNet.Attributes;\n<span class=\"hljs-keyword\">using<\/span> System.Text.Json;\n\n&#91;<span class=\"hljs-meta\">MemoryDiagnoser<\/span>]\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">SerializationBenchmarks<\/span>\n{\n    <span class=\"hljs-keyword\">private<\/span> <span class=\"hljs-keyword\">readonly<\/span> PricingEvent _event = <span class=\"hljs-keyword\">new<\/span>()\n    {\n        Symbol = <span class=\"hljs-string\">\"EURUSD\"<\/span>,\n        Bid = <span class=\"hljs-number\">1.0825<\/span>m,\n        Ask = <span class=\"hljs-number\">1.0827<\/span>m,\n        Timestamp = DateTimeOffset.UtcNow\n    };\n\n    &#91;<span class=\"hljs-meta\">Benchmark(Baseline = true)<\/span>]\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">string<\/span> <span class=\"hljs-title\">ReflectionPath<\/span>(<span class=\"hljs-params\"><\/span>)<\/span>\n        =&gt; JsonSerializer.Serialize(_event);\n\n    &#91;<span class=\"hljs-meta\">Benchmark<\/span>]\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">string<\/span> <span class=\"hljs-title\">SourceGenPath<\/span>(<span class=\"hljs-params\"><\/span>)<\/span>\n        =&gt; JsonSerializer.Serialize(_event, PricingJsonContext.Default.PricingEvent);\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This isolates the first optimization: removing runtime metadata discovery. In most real services, that reduces allocations and trims CPU, but the bigger operational win is often <strong>less GC interference<\/strong>, not dramatically lower per-call latency.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The next benchmark should test the more production-relevant path: avoiding the intermediate <code>string<\/code>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-26\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">using<\/span> BenchmarkDotNet.Attributes;\n<span class=\"hljs-keyword\">using<\/span> System.Buffers;\n<span class=\"hljs-keyword\">using<\/span> System.Text.Json;\n\n&#91;<span class=\"hljs-meta\">MemoryDiagnoser<\/span>]\n<span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">Utf8Benchmarks<\/span>\n{\n    <span class=\"hljs-keyword\">private<\/span> <span class=\"hljs-keyword\">readonly<\/span> PricingEvent _event = <span class=\"hljs-keyword\">new<\/span>();\n\n    &#91;<span class=\"hljs-meta\">Benchmark<\/span>]\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">byte<\/span>&#91;] <span class=\"hljs-title\">SerializeToUtf8Buffer<\/span>(<span class=\"hljs-params\"><\/span>)<\/span>\n    {\n        <span class=\"hljs-keyword\">var<\/span> buffer = <span class=\"hljs-keyword\">new<\/span> ArrayBufferWriter&lt;<span class=\"hljs-keyword\">byte<\/span>&gt;(<span class=\"hljs-number\">256<\/span>);\n        <span class=\"hljs-keyword\">using<\/span> <span class=\"hljs-keyword\">var<\/span> writer = <span class=\"hljs-keyword\">new<\/span> Utf8JsonWriter(buffer);\n\n        JsonSerializer.Serialize(writer, _event, PricingJsonContext.Default.PricingEvent);\n        <span class=\"hljs-keyword\">return<\/span> buffer.WrittenSpan.ToArray();\n    }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-26\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Yes, <code>ToArray()<\/code> still allocates\u2014but this benchmark helps separate <strong>serializer cost<\/strong> from <strong>transport integration cost<\/strong>. In production, you would often write those bytes directly to a response body or pipe instead.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">One more optimization worth testing is option reuse plus centralized context wiring:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-27\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\">builder.Services.ConfigureHttpJsonOptions(options =&gt;\n{\n    options.SerializerOptions.TypeInfoResolverChain.Insert(<span class=\"hljs-number\">0<\/span>, PricingJsonContext.Default);\n});\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-27\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">That does not make each individual serialization magically faster than the explicit overload, but it prevents inconsistent paths across the application. In production, consistency is its own optimization: fewer accidental fallbacks, fewer surprises under load, and much more trustworthy benchmark results.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">7. Common Pitfalls and Debugging Tips<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The most common production failure is not \u201csource generators are broken.\u201d It is <strong>thinking you\u2019re using them when you\u2019re not<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A classic example is declaring a context but then calling the wrong overload:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-28\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-comment\">\/\/ Looks harmless, but this can use the default runtime path<\/span>\n<span class=\"hljs-keyword\">var<\/span> json = JsonSerializer.Serialize(order);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-28\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">If this code sits in a hot path, your optimization is effectively gone. The fix is explicit usage:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-29\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-keyword\">var<\/span> json = JsonSerializer.Serialize(\n    order,\n    OrderJsonContext.Default.OrderResponse);\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-29\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">That extra argument is not noise. It is the proof that the generated contract is being used.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Another common issue is forgetting nested or collection-heavy types. Teams add the root DTO and assume the graph is fully covered:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-30\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\">&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(OrderResponse))<\/span>]\n<span class=\"hljs-keyword\">internal<\/span> <span class=\"hljs-keyword\">partial<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">OrderJsonContext<\/span> : <span class=\"hljs-title\">JsonSerializerContext<\/span>\n{\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-30\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">In practice, large payloads often include dictionaries, lists, or auxiliary types that deserve explicit coverage:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-31\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\">&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(OrderResponse))<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(OrderLine))<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(List&lt;OrderLine&gt;))<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(Dictionary&lt;string, string&gt;))<\/span>]\n<span class=\"hljs-keyword\">internal<\/span> <span class=\"hljs-keyword\">partial<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">OrderJsonContext<\/span> : <span class=\"hljs-title\">JsonSerializerContext<\/span>\n{\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-31\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">If performance suddenly regresses after a schema change, this is one of the first places I check.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Finally, inspect generated output instead of guessing. Turn this on in your project file:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-32\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\">&lt;PropertyGroup&gt;\n  &lt;EmitCompilerGeneratedFiles&gt;<span class=\"hljs-literal\">true<\/span>&lt;\/EmitCompilerGeneratedFiles&gt;\n  &lt;CompilerGeneratedFilesOutputPath&gt;Generated&lt;\/CompilerGeneratedFilesOutputPath&gt;\n&lt;\/PropertyGroup&gt;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-32\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">That gives you something concrete to verify during migration. In production work, debugging serializers by assumption is a bad habit. Generated code, benchmark output, and allocation traces are much more trustworthy than \u201cit should be using the fast path.\u201d<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">8. Production Best Practices<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">By the time source-generated serialization reaches production, the main goal is no longer \u201cmake it faster.\u201d It is <strong>make it hard to accidentally get slower again<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First, centralize your JSON contract instead of scattering context classes across random projects:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-33\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\">&#91;<span class=\"hljs-meta\">JsonSourceGenerationOptions(\n    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,\n    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(OrderResponse))<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(ErrorResponse))<\/span>]\n&#91;<span class=\"hljs-meta\">JsonSerializable(typeof(List&lt;OrderResponse&gt;))<\/span>]\n<span class=\"hljs-keyword\">internal<\/span> <span class=\"hljs-keyword\">partial<\/span> <span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">ApiJsonContext<\/span> : <span class=\"hljs-title\">JsonSerializerContext<\/span>\n{\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-33\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This makes serializer coverage reviewable. In real systems, that matters more than shaving a few nanoseconds off a benchmark.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Second, wire the context globally so framework-managed serialization does not quietly drift back to reflection:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-34\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\">builder.Services.ConfigureHttpJsonOptions(options =&gt;\n{\n    options.SerializerOptions.TypeInfoResolverChain.Insert(<span class=\"hljs-number\">0<\/span>, ApiJsonContext.Default);\n});\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-34\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">That pattern is used in production because hand-optimizing one endpoint is easy; keeping an entire service consistent is harder.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Third, keep hot paths byte-oriented:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-35\" data-shcb-language-name=\"C#\" data-shcb-language-slug=\"cs\"><span><code class=\"hljs language-cs\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">public<\/span> <span class=\"hljs-keyword\">static<\/span> Task <span class=\"hljs-title\">WriteAsync<\/span>(<span class=\"hljs-params\">Stream output, OrderResponse response, CancellationToken ct<\/span>)<\/span>\n{\n    <span class=\"hljs-keyword\">return<\/span> JsonSerializer.SerializeAsync(\n        output,\n        response,\n        ApiJsonContext.Default.OrderResponse,\n        ct);\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-35\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">C#<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">cs<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This avoids unnecessary <code>string<\/code> creation and fits naturally with HTTP responses, pipes, and stream-based exporters.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The trade-off across all of this is maintenance. Every new DTO added to a critical path should be treated as part of the serialization contract. But that is a good trade. In production, explicit contracts, centralized configuration, and byte-first output are exactly how you keep performance gains from evaporating six months later.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>1. Real-World Problem Scenario A few months ago, we were tuning a high-throughput .NET API responsible for ingesting pricing events\u2014nothing exotic, just a stream of relatively small DTOs (~200\u2013400 bytes each). The service itself was straightforward: deserialize request \u2192 process \u2192 serialize response. On paper, this shouldn\u2019t have been the bottleneck. But under sustained load [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_genesis_hide_title":false,"_genesis_hide_breadcrumbs":false,"_genesis_hide_singular_image":false,"_genesis_hide_footer_widgets":false,"_genesis_custom_body_class":"","_genesis_custom_post_class":"","_genesis_layout":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[8,4],"tags":[],"class_list":["post-2291","post","type-post","status-publish","format-standard","category-csharp","category-programming-languages","entry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Zero-Allocation Serialization with System.Text.Json Source Generators<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Zero-Allocation Serialization with System.Text.Json Source Generators\" \/>\n<meta property=\"og:description\" content=\"1. Real-World Problem Scenario A few months ago, we were tuning a high-throughput .NET API responsible for ingesting pricing events\u2014nothing exotic, just a stream of relatively small DTOs (~200\u2013400 bytes each). The service itself was straightforward: deserialize request \u2192 process \u2192 serialize response. On paper, this shouldn\u2019t have been the bottleneck. But under sustained load [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/\" \/>\n<meta property=\"article:published_time\" content=\"2026-04-08T09:12:53+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-04-08T09:32:05+00:00\" \/>\n<meta name=\"author\" content=\"w3compadmin\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"w3compadmin\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"13 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"TechArticle\",\"@id\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/zero-allocation-serialization-with-system-text-json-source-generators\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/zero-allocation-serialization-with-system-text-json-source-generators\\\/\"},\"author\":{\"name\":\"w3compadmin\",\"@id\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/#\\\/schema\\\/person\\\/a550b3e20d78bb4f79b7c6b7b53f0561\"},\"headline\":\"Zero-Allocation Serialization with System.Text.Json Source Generators\",\"datePublished\":\"2026-04-08T09:12:53+00:00\",\"dateModified\":\"2026-04-08T09:32:05+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/zero-allocation-serialization-with-system-text-json-source-generators\\\/\"},\"wordCount\":2842,\"articleSection\":[\"C#\",\"Programming Languages\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/zero-allocation-serialization-with-system-text-json-source-generators\\\/\",\"url\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/zero-allocation-serialization-with-system-text-json-source-generators\\\/\",\"name\":\"Zero-Allocation Serialization with System.Text.Json Source Generators\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/#website\"},\"datePublished\":\"2026-04-08T09:12:53+00:00\",\"dateModified\":\"2026-04-08T09:32:05+00:00\",\"author\":{\"@id\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/#\\\/schema\\\/person\\\/a550b3e20d78bb4f79b7c6b7b53f0561\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/zero-allocation-serialization-with-system-text-json-source-generators\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/zero-allocation-serialization-with-system-text-json-source-generators\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/zero-allocation-serialization-with-system-text-json-source-generators\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Articles Home\",\"item\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Programming Languages\",\"item\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/programming-languages\\\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Zero-Allocation Serialization with System.Text.Json Source Generators\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/#website\",\"url\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/\",\"name\":\"Developer Articles Hub\",\"description\":\"\",\"alternateName\":\"Developer Articles\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/#\\\/schema\\\/person\\\/a550b3e20d78bb4f79b7c6b7b53f0561\",\"name\":\"w3compadmin\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/wp-content\\\/litespeed\\\/avatar\\\/bd481d404e42caa2763662a3bfe825f8.jpg?ver=1781352167\",\"url\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/wp-content\\\/litespeed\\\/avatar\\\/bd481d404e42caa2763662a3bfe825f8.jpg?ver=1781352167\",\"contentUrl\":\"https:\\\/\\\/www.w3computing.com\\\/articles\\\/wp-content\\\/litespeed\\\/avatar\\\/bd481d404e42caa2763662a3bfe825f8.jpg?ver=1781352167\",\"caption\":\"w3compadmin\"},\"sameAs\":[\"http:\\\/\\\/w3computing.com\\\/articles\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Zero-Allocation Serialization with System.Text.Json Source Generators","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/","og_locale":"en_US","og_type":"article","og_title":"Zero-Allocation Serialization with System.Text.Json Source Generators","og_description":"1. Real-World Problem Scenario A few months ago, we were tuning a high-throughput .NET API responsible for ingesting pricing events\u2014nothing exotic, just a stream of relatively small DTOs (~200\u2013400 bytes each). The service itself was straightforward: deserialize request \u2192 process \u2192 serialize response. On paper, this shouldn\u2019t have been the bottleneck. But under sustained load [&hellip;]","og_url":"https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/","article_published_time":"2026-04-08T09:12:53+00:00","article_modified_time":"2026-04-08T09:32:05+00:00","author":"w3compadmin","twitter_card":"summary_large_image","twitter_misc":{"Written by":"w3compadmin","Est. reading time":"13 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"TechArticle","@id":"https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/#article","isPartOf":{"@id":"https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/"},"author":{"name":"w3compadmin","@id":"https:\/\/www.w3computing.com\/articles\/#\/schema\/person\/a550b3e20d78bb4f79b7c6b7b53f0561"},"headline":"Zero-Allocation Serialization with System.Text.Json Source Generators","datePublished":"2026-04-08T09:12:53+00:00","dateModified":"2026-04-08T09:32:05+00:00","mainEntityOfPage":{"@id":"https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/"},"wordCount":2842,"articleSection":["C#","Programming Languages"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/","url":"https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/","name":"Zero-Allocation Serialization with System.Text.Json Source Generators","isPartOf":{"@id":"https:\/\/www.w3computing.com\/articles\/#website"},"datePublished":"2026-04-08T09:12:53+00:00","dateModified":"2026-04-08T09:32:05+00:00","author":{"@id":"https:\/\/www.w3computing.com\/articles\/#\/schema\/person\/a550b3e20d78bb4f79b7c6b7b53f0561"},"breadcrumb":{"@id":"https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.w3computing.com\/articles\/zero-allocation-serialization-with-system-text-json-source-generators\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Articles Home","item":"https:\/\/www.w3computing.com\/articles\/"},{"@type":"ListItem","position":2,"name":"Programming Languages","item":"https:\/\/www.w3computing.com\/articles\/programming-languages\/"},{"@type":"ListItem","position":3,"name":"Zero-Allocation Serialization with System.Text.Json Source Generators"}]},{"@type":"WebSite","@id":"https:\/\/www.w3computing.com\/articles\/#website","url":"https:\/\/www.w3computing.com\/articles\/","name":"Developer Articles Hub","description":"","alternateName":"Developer Articles","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.w3computing.com\/articles\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/www.w3computing.com\/articles\/#\/schema\/person\/a550b3e20d78bb4f79b7c6b7b53f0561","name":"w3compadmin","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.w3computing.com\/articles\/wp-content\/litespeed\/avatar\/bd481d404e42caa2763662a3bfe825f8.jpg?ver=1781352167","url":"https:\/\/www.w3computing.com\/articles\/wp-content\/litespeed\/avatar\/bd481d404e42caa2763662a3bfe825f8.jpg?ver=1781352167","contentUrl":"https:\/\/www.w3computing.com\/articles\/wp-content\/litespeed\/avatar\/bd481d404e42caa2763662a3bfe825f8.jpg?ver=1781352167","caption":"w3compadmin"},"sameAs":["http:\/\/w3computing.com\/articles"]}]}},"featured_image_src":null,"featured_image_src_square":null,"author_info":{"display_name":"w3compadmin","author_link":"https:\/\/www.w3computing.com\/articles\/author\/w3compadmin\/"},"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.w3computing.com\/articles\/wp-json\/wp\/v2\/posts\/2291","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.w3computing.com\/articles\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.w3computing.com\/articles\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.w3computing.com\/articles\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.w3computing.com\/articles\/wp-json\/wp\/v2\/comments?post=2291"}],"version-history":[{"count":7,"href":"https:\/\/www.w3computing.com\/articles\/wp-json\/wp\/v2\/posts\/2291\/revisions"}],"predecessor-version":[{"id":2298,"href":"https:\/\/www.w3computing.com\/articles\/wp-json\/wp\/v2\/posts\/2291\/revisions\/2298"}],"wp:attachment":[{"href":"https:\/\/www.w3computing.com\/articles\/wp-json\/wp\/v2\/media?parent=2291"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.w3computing.com\/articles\/wp-json\/wp\/v2\/categories?post=2291"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.w3computing.com\/articles\/wp-json\/wp\/v2\/tags?post=2291"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}