{"id":48122,"date":"2023-10-05T10:05:00","date_gmt":"2023-10-05T17:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=48122"},"modified":"2023-10-09T16:48:52","modified_gmt":"2023-10-09T23:48:52","slug":"the-convenience-of-system-text-json","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/the-convenience-of-system-text-json\/","title":{"rendered":"The convenience of System.Text.Json"},"content":{"rendered":"<p><a href=\"https:\/\/learn.microsoft.com\/dotnet\/standard\/serialization\/system-text-json\/overview\">JSON document processing<\/a> is one of the most common tasks when working on a modern codebase, appearing equally in client and cloud apps. <a href=\"https:\/\/learn.microsoft.com\/dotnet\/standard\/serialization\/system-text-json\/how-to\"><code>System.Text.Json<\/code><\/a> offers multiple APIs for reading and writing JSON documents. In this post, we&#8217;re going to look at the convenience of reading and writing JSON with <code>System.Text.Json<\/code>. We&#8217;ll also look at <a href=\"https:\/\/www.newtonsoft.com\/json\"><code>Newtonsoft.Json<\/code><\/a> (AKA Json.NET), the original popular and capable JSON library for .NET.<\/p>\n<p>We recently kicked off a series on the <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/the-convenience-of-dotnet\/\">Convenience of .NET<\/a> that describes our approach for providing convenient solutions to common tasks. The key message behind this series is that one of the strengths of the .NET platform is that it provides APIs that appeal to a broad set of developers with a broad set of needs. This range of APIs can be thought of as beginner to advanced, however I prefer to think of the range as convenient with great default behaviors to flexible with low-level control. <code>System.Text.Json<\/code> is a good example of such a wide-ranging API family.<\/p>\n<p>Please check out <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/system-text-json-in-dotnet-8\/\">What\u2019s new in System.Text.Json in .NET 8<\/a>. Some of these new JSON features are used in the code that we&#8217;ll be analyzing.<\/p>\n<h2>The APIs<\/h2>\n<p>JSON processing has a few common flavors. <strong>Serializer<\/strong> APIs automatically serialize and deserialize JSON, converting objects-to-JSON and JSON-to-objects, respectively. <strong>Document Object Model (DOM)<\/strong> APIs provide a view of an entire JSON document, with straightforward patterns for reading and writing objects, arrays, and other JSON data types. Last are <strong>reader and writer<\/strong> APIs that enable reading and writing JSON documents, one JSON node at time, with maximum performance and flexibility.<\/p>\n<p>These are the APIs we&#8217;re going to analyze (covering all three of those flavors):<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.text.json.jsonserializer\"><code>System.Text.Json.JsonSerializer<\/code><\/a><\/li>\n<li><a href=\"https:\/\/www.newtonsoft.com\/json\/help\/html\/T_Newtonsoft_Json_JsonSerializer.htm\"><code>Newtonsoft.Json.JsonSerializer<\/code><\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.text.json.nodes.jsonnode\"><code>System.Text.Json.Nodes.JsonNode<\/code><\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.text.json.utf8jsonreader\"><code>System.Text.Json.Utf8JsonReader<\/code><\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.text.json.utf8jsonwriter\"><code>System.Text.Json.Utf8JsonWriter<\/code><\/a><\/li>\n<\/ul>\n<p>Note: <code>Newtonsoft.Json<\/code> also offers <a href=\"https:\/\/www.newtonsoft.com\/json\/help\/html\/T_Newtonsoft_Json_Linq_JObject.htm\">DOM<\/a>, <a href=\"https:\/\/www.newtonsoft.com\/json\/help\/html\/T_Newtonsoft_Json_JsonReader.htm\">reader<\/a>, and <a href=\"https:\/\/www.newtonsoft.com\/json\/help\/html\/T_Newtonsoft_Json_JsonWriter.htm\">writer<\/a> APIs. <code>System.Text.Json<\/code> also offers a read-only DOM API with <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.text.json.jsondocument\"><code>System.Text.Json.JsonDocument<\/code><\/a>. This post doesn&#8217;t look at those, however they all offer valuable capapabilities.<\/p>\n<p>Next, we&#8217;ll look at an app that has been implemented multiple times &#8212; for each of those APIs &#8212; testing their approachability and efficiency. <\/p>\n<h2>The app<\/h2>\n<p><a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/releasejson\/\">The app<\/a> generates a sort of JSON <a href=\"https:\/\/gist.github.com\/richlander\/4701a33592abd021f767644974c0ced6\">compliance summary<\/a> from the <a href=\"https:\/\/github.com\/dotnet\/core\/blob\/main\/release-notes\/releases-index.json\">JSON files that we publish for .NET releases<\/a>. One of the goals of this blog series was to write small apps we could test for performance and that others might find useful.<\/p>\n<p>The report has a few requirements:<\/p>\n<ul>\n<li>Include the latest patch release and latest security release for each major version, including the list of CVEs.<\/li>\n<li>Include days to or from important events.<\/li>\n<li>Match the <a href=\"https:\/\/github.com\/dotnet\/core\/blob\/main\/release-notes\/6.0\/releases.json\"><code>releases.json<\/code><\/a> <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/whats-new\/dotnet-8#naming-policies\">kebab-case<\/a> schema as much as possible.<\/li>\n<\/ul>\n<p>The app writes the JSON report to the console, but only in <code>Debug<\/code> mode (to avoid affecting performance measurement).<\/p>\n<p>These benchmarks produce reports for a single .NET version, for simplicity. I wrote <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/blob\/main\/samples\/releasesapp\/README.md\">another sample<\/a> that generates a report for multiple releases. If you want to use the app for its stated compliance purpose, I&#8217;d recommend using that one.<\/p>\n<h2>Methodology<\/h2>\n<p>The app has been tested with <a href=\"https:\/\/github.com\/richlander\/convenience\/tree\/json\/releasejson\/fakejson\">multiple JSON files<\/a>, all using the same schema, but differing dramatically in size (1k vs 1000k) and where the target data exists within the file (start vs end). The tests were also run on two machine types.<\/p>\n<p>You can run these same measurements yourself with the test app. Note that the measurements include calling the network (just like apps in the real world do) so network conditions will affect the results.<\/p>\n<p>All tests were run in release mode (<code>dotnet run -c Release<\/code>). I used an <code>rtm<\/code> branch .NET 8 <a href=\"https:\/\/github.com\/dotnet\/installer#installers-and-binaries\">nightly build<\/a>. That means a build that is as close to the final .NET 8 GA build as possible (at the time of writing).<\/p>\n<p>I ran the majority of performance tests on an Ubuntu 22.04 x64 machine. <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-6-is-now-in-ubuntu-2204\/\">.NET is now built into Ubuntu<\/a>, however that doesn&#8217;t help with testing a nightly build.<\/p>\n<p>Here&#8217;s the pattern for using a nightly build:<\/p>\n<pre><code class=\"language-bash\">rich@vancouver:~$ mkdir dotnet\nrich@vancouver:~$ curl -LO https:\/\/aka.ms\/dotnet\/8.0.1xx\/daily\/dotnet-sdk-linux-x64.tar.gz\n  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed\n  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0\n100  204M  100  204M    0     0  61.7M      0  0:00:03  0:00:03 --:--:-- 70.9M\nrich@vancouver:~$ tar -C dotnet\/ -xf dotnet-sdk-linux-x64.tar.gz \nrich@vancouver:~$ export PATH=~\/dotnet:$PATH\nrich@vancouver:~$ dotnet --version\n8.0.100-rtm.23502.10<\/code><\/pre>\n<p>Note: A <a href=\"https:\/\/gist.github.com\/richlander\/4a700d1679e42b7868805c0780ab173c\">nuget.config<\/a> files is often needed when using a nightly build.<\/p>\n<p>My test machine is an 8-core <a href=\"https:\/\/en.wikichip.org\/wiki\/intel\/core_i7\/i7-6700\">i7-6700<\/a>, from the Skylake era. That&#8217;s not new, which means that newer machines should produce faster results.<\/p>\n<pre><code class=\"language-bash\">rich@vancouver:~$ cat \/proc\/cpuinfo | grep \"model name\" | head -n 1\nmodel name  : Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz\nrich@vancouver:~$ cat \/proc\/cpuinfo | grep \"model name\" | wc -l\n8<\/code><\/pre>\n<p>Note: This machine is a dedicated and headless test machine, with nothing else running. I access it remotely, from home, Microsoft campus or on the road, via a combination of <a href=\"https:\/\/vscode.dev\/\">vscode.dev<\/a> (<a href=\"https:\/\/code.visualstudio.com\/docs\/remote\/tunnels\">tunnels<\/a>) and <a href=\"https:\/\/tailscale.com\/tailscale-ssh\/\">Tailscale SSH<\/a>. We&#8217;re truly spoiled with the great remote development options available today.<\/p>\n<p>The tests were run on home internet via wired ethernet.<\/p>\n<h2>Results<\/h2>\n<p>Each implementation has been measured in terms of:<\/p>\n<ul>\n<li>Lines of code<\/li>\n<li>Speed of execution<\/li>\n<li>Memory use<\/li>\n<\/ul>\n<h3>Lines of code<\/h3>\n<p>I love solutions that are easy and approchable. Lines of code is our best proxy metric for that.<\/p>\n<p><img decoding=\"async\" title=\"Json Serializers -- lines of code to use them\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/json-api-loc.png\" width=\"75%\" \/><\/p>\n<p>These measurement are for the whole app, including the types defined for the serializer. The code is written in an idiomatic way with healthy use of newer (terse) syntax.<\/p>\n<p>The chart tells a clear story. <code>JsonSerializer<\/code> (both of them) and <code>JsonNode<\/code> are the most convenient APIs (based on line count). <code>JsonSerializer<\/code> is an easy choice if you have types defined for the JSON you want to read or write. These days, it&#8217;s convenient to quickly create a set of types to model your JSON domain with the introduction of <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/c-9-0-on-the-record\/\"><code>record<\/code> types<\/a>. In fact, the app uses <code>record<\/code> types for that reason. Otherwise, the lines of code difference between <code>JsonSerializer<\/code> and <code>JsonNode<\/code> isn&#8217;t all that meaningful. It&#8217;s more of a question if you like using an automatic serializer or a DOM API. I am happy using either of them.<\/p>\n<p>The <code>Utf8JsonReader<\/code> API is our low-level workhorse API. It&#8217;s actually what <code>JsonSerializer<\/code> and <code>JsonNode<\/code> are built on. It is a great choice if you want more control over how a JSON document is read, for example to skip parts of it. The API assumes a deep understanding of .NET and JSON type systems and how to write low-level reliable code. The higher line count is a direct result of that.<\/p>\n<p><code>JsonSerializer<\/code> and <code>JsonNode<\/code> are clearly the default options since they don&#8217;t require much code to write a JSON-driven algorithm. Let&#8217;s see if there is a compelling reason to consider <code>Utf8JsonReader<\/code> in the performance measurements, since the cost of getting something working is much higher.<\/p>\n<h3>Small document<\/h3>\n<p>The first performance test uses a <a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/fakejson\/fake-one-release-only.json\">small test document<\/a>, requested from a remote URL. It is is 905 bytes and describes a single .NET 6 release.<\/p>\n<p><img decoding=\"async\" title=\"Performance speed results for small JSON file\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/json-api-speed-small-document-remote.png\" width=\"75%\" \/><\/p>\n<p>The APIs are tied in a dead heat! That&#8217;s a shock, right? All of these APIs are very good, but we should expect more of a difference. My theory (which should play out in the rest of the analysis) is that they are all equally waiting on the network. Put another way, the CPU is more than able to keep up with the network and any differences between the implementations is hidden by the dominant network cost.<\/p>\n<p>As an aside, I added some logging to the <code>Utf8JsonReader<\/code> implementation (to diagnose challenges in my own code; since removed) and discovered that the code was waiting on the network more than I would have guessed. Unsurprisingly, modern CPUs can outrun (often unreliable) networks.<\/p>\n<p>50ms of compute is a lot, particularly for only 905 bytes of JSON. Let&#8217;s try some local access options.<\/p>\n<p><img decoding=\"async\" title=\"Performance speed results for small JSON file\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/json-api-speed-small-document-local.png\" width=\"75%\" \/><\/p>\n<p>OK. These numbers look much better. We&#8217;re now well into sub-millisecond results. To be clear, I only changed the source of the data. The implementations are all based on <code>Stream<\/code> so it is easy to switch out one <code>Stream<\/code> producer for another.<\/p>\n<p>The numbers are still pretty close. My theory there is that&#8217;s because this document is so small.<\/p>\n<p>I tried three different local options for this test so that we could really focus in on the performance of the APIs.<\/p>\n<ul>\n<li><strong>Local Web<\/strong>: Reads the JSON from a local ASP.NET Core app &#8212; via <code>http:\/\/localhost:5255<\/code> &#8212;  on the same machine.<\/li>\n<li><strong>Local File<\/strong>: Reads the JSON from the file system.<\/li>\n<li><strong>Local File M1<\/strong>: Same, but run on an M1 Mac (Arm64).<\/li>\n<\/ul>\n<p>The first two tests were run on my Intel i7 machine. The last was run on my MacBook Air M1 laptop (connected to power).<\/p>\n<p>The key takeaway is that these APIs can run very fast when they are kept busy with data to process. That&#8217;s the effective difference between the remote and local numbers. The Apple M1 performance numbers tells us that <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/this-arm64-performance-in-dotnet-8\/\">.NET performance on Arm64<\/a> is very good at this point. Those numbers are shockingly good.<\/p>\n<p>What&#8217;s up with <code>Utf8JsonReader<\/code>? It&#8217;s supposed to be <em>really<\/em> fast, right? Ha! Just wait, just wait.<\/p>\n<p>I tried a <a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/fakejson\/fake-releases-compact.json\">medium size document<\/a> &#8212; <code>9.41<\/code> kB &#8212; and found no significant difference.<\/p>\n<p>Let&#8217;s look at memory usage for the small document test, using <code>Environment.WorkingSet<\/code>.<\/p>\n<p><img decoding=\"async\" title=\"Performance memory results for small JSON file\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/json-api-memory-small-document.png\" width=\"75%\" \/><\/p>\n<p>Here, we&#8217;re seeing clustering among the <code>System.Text.Json<\/code> APIs with <code>Newtonsoft.Json<\/code> as the outlier. Let&#8217;s be very clear at what is going on here. <code>System.Text.Json<\/code> was built a decade or so after <code>Newtonsoft.Json<\/code> and had the benefit of using a whole new set of platform APIs oriented on making high-performance code much easier to write, for both speed and memory usage.<\/p>\n<p>The .NET Core era is a sort of <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-net-8\/\">performance renaissance for .NET<\/a> and it shows.<\/p>\n<p>In particular, the <code>System.Text.Json<\/code> implementations are oriented on 8-bit Unicode characters not the 16-bit Unicode characters you get with the <code>string<\/code> and <code>char<\/code> types. That&#8217;s why the reader is called <strong>Utf8<\/strong>JsonReader. That means that <code>System.Text.Json<\/code> is able to maintain the document data at &#8220;half price&#8221; until JSON string data needs to be materialized as a UTF16 .NET <code>string<\/code>, if at all.<\/p>\n<h3>Large document<\/h3>\n<p>The next performance test uses a <a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/fakejson\/fake-releases.json\">larger test document<\/a>. It is <code>1.17 MB<\/code> and is (a copy of) the official <a href=\"https:\/\/github.com\/dotnet\/core\/blob\/main\/release-notes\/6.0\/releases.json\"><code>release.json<\/code> file for .NET 6<\/a>.<\/p>\n<p><img decoding=\"async\" title=\"Performance results for large JSON file\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/json-api-speed-large-document-remote.png\" width=\"75%\" \/><\/p>\n<p>Wow! That&#8217;s a big difference. Hold up. Why is <code>Utf8Reader<\/code> performance so much better with this large document?<\/p>\n<p>In short, the app only needs data from the first 1k of the document and our <code>Utf8JsonReader<\/code> implementation is best able to take advantage of that. This is a case where my app (and the JSON it processes) may be different than yours.<\/p>\n<p>The app is looking within the JSON document for the first <code>\"security\": true<\/code> patch release for .NET 6. If you&#8217;re familiar with our patch releases, you&#8217;ll know we ship a lot of security updates. That means that it is likely that the most recent patch release is a security release and that most of the 1MB+ document does not need to be read. That style of data massively favors a manual serialization approach.<\/p>\n<p>If you have large JSON documents where only slices of them need to be read, then this finding may be relevant to you. Manual and automatic serialization can be <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.text.json.jsonserializer.deserialize\">mixed and matched<\/a>, but in somewhat limited ways. I&#8217;d love to provide <code>JsonSerializer<\/code> a <code>Utf8JsonReader<\/code> and get it to return an <code>IAsyncEnumerable&lt;MyObject&gt;<\/code> at some mid-point in a large JSON document. That&#8217;s not currently possible.<\/p>\n<p>Let&#8217;s try the same tests locally again.<\/p>\n<p><img decoding=\"async\" title=\"Performance results for large JSON file\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/json-api-speed-large-document-local.png\" width=\"75%\" \/><\/p>\n<p>This is where we&#8217;re really seeing the <code>System.Text.Json<\/code> family shine. Again, these APIs are able to crunch through JSON data quickly, particularly when it close by.<\/p>\n<p>Let&#8217;s look at memory usage.<\/p>\n<p><img decoding=\"async\" title=\"Performance results for large JSON file\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/json-api-memory-large-document.png\" width=\"75%\" \/><\/p>\n<p>These results are roughly similar to the clustering we saw with the small document, but JsonNode seems to be more affected by the target data being so far into the document.<\/p>\n<h3>Large document &#8212; Stress test<\/h3>\n<p>I <a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/fakejson\/fake-release-near-end.json\">modified the document<\/a> so that the latest security patch was much further back in time. I pushed it back almost two years, all the way to the <code>6.0.1<\/code> release (as compared to <a href=\"https:\/\/github.com\/dotnet\/core\/blob\/main\/release-notes\/6.0\/releases.json\">6.0.20+ now<\/a>), to see how that changed the results. That means that the <code>Utf8JsonReader<\/code> has to do a lot more work (a kind of relative stress test), possibly more similar to the other implementations. That should show up in the results.<\/p>\n<p><img decoding=\"async\" title=\"Performance results for large JSON file\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/json-api-speed-large-document2-remote.png\" width=\"75%\" \/><\/p>\n<p>All the implementations are taking longer since most of the JSON needs to be read. The performance order is still the same, but the values are a lot tighter.<\/p>\n<p><img decoding=\"async\" title=\"Performance results for large JSON file\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/json-api-speed-large-document2-local.png\" width=\"75%\" \/><\/p>\n<p>That&#8217;s the local performance. The values are a lot slower than the previous two documents (for the stated reasons), however, the <code>System.Text.Json<\/code> implementations are able to quickly process the megabyte of JSON.<\/p>\n<h3>Results Summary<\/h3>\n<p>There are a few findings that pop out of this data:<\/p>\n<ul>\n<li>The primary and consistent win from <code>System.Text.Json<\/code> is lower memory use.<\/li>\n<li><code>JsonSerializer<\/code> and <code>Utf8JsonReader<\/code> are both great tools for specific jobs, however, <code>JsonSerializer<\/code> should be more than good enough in the vast majority of situations.<\/li>\n<li><code>Newtonsoft.Json<\/code> is competitive on speed with <code>System.Text.Json<\/code> for internet web URLs (where both APIs are likely waiting on the network). <code>System.Text.Json<\/code> will consistently win when data can be served up faster.<\/li>\n<\/ul>\n<p>Let&#8217;s move to a more detailed analysis.<\/p>\n<h2>JsonSerializer<\/h2>\n<p><code>System.Text.Json.JsonSerializer<\/code> is the high-level automatic serialization solution that comes with .NET. In the last couple releases, we&#8217;ve been improving it with a <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/try-the-new-system-text-json-source-generator\/\">companion source generator<\/a>. <code>JsonSerializer<\/code> is approachable and straightforward to use, while the source generator removes reflection usage, which is critical for <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/deploying\/trimming\/trim-self-contained\">trimming<\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/deploying\/native-aot\/\">native AOT<\/a>.<\/p>\n<p>Implementations:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/releasejson\/JsonSerializerBenchmark.cs\"><code>JsonSerializerBenchmark<\/code><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/releasejson\/JsonSerializerSourceGeneratorRecordBenchmark.cs\"><code>JsonSerializerSourceGeneratorRecordBenchmark<\/code> with source generation using records<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/releasejson\/JsonSerializerSourceGeneratorPocoBenchmark.cs\"><code>JsonSerializerSourceGeneratorPocoBenchmark<\/code> with source generation using regular classes<\/a><\/li>\n<\/ul>\n<p>Note: <a href=\"https:\/\/en.wikipedia.org\/wiki\/Plain_old_CLR_object\">POCO<\/a> == &#8220;plain old CLR object&#8221;.<\/p>\n<p>I wrote three implementations with <code>JsonSerializer<\/code> to evaluate if there were significant approachability or efficiency differences when using source generation and not. There weren&#8217;t.<\/p>\n<p>Let&#8217;s see what the numbers tell us, with the small document, on my Apple M1 machine.<\/p>\n<p><img decoding=\"async\" title=\"JsonSerializer performance results for small JSON file on MacBook M1\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/jsonserializer-api-speed-small-document-local.png\" width=\"75%\" \/><\/p>\n<p>That&#8217;s a pretty tight range (for each measurement type). I added <code>string<\/code> as a new measurement type. It brings the JSON one step closer to the API since it is loaded in memory before the benchmark runs.<\/p>\n<p>Here&#8217;s the same thing with the bigger 1MB+ document.<\/p>\n<p><img decoding=\"async\" title=\"JsonSerializer performance results for small JSON file on MacBook M1\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/jsonserializer-api-speed-large-document-local.png\" width=\"75%\" \/><\/p>\n<p>Again, we&#8217;re seeing that the API performs much better as the data gets closer. We&#8217;re also seeing that the source generator provides some advantage, but not much.<\/p>\n<p>Let&#8217;s move onto the code.<\/p>\n<p>The top-level method is pretty compact.<\/p>\n<pre><code class=\"language-csharp\">public static async Task&lt;int&gt; MakeReportWebAsync(string url)\n{\n    using HttpClient httpClient= new();\n    MajorRelease release = await httpClient.GetFromJsonAsync&lt;MajorRelease&gt;(url, _options) ?? throw new Exception(Error.BADJSON);\n    Report report = new(DateTime.Today.ToShortDateString(), [ GetVersion(release) ]);\n    string reportJson =  JsonSerializer.Serialize(report, _options);\n    WriteJsonToConsole(reportJson);\n    return reportJson.Length;\n}<\/code><\/pre>\n<p>There are three aspects to call out that make this implementation convenient to use.<\/p>\n<ul>\n<li>The <code>GetFromJsonAsync<\/code> extension method on <code>HttpClient<\/code> provides a nice one-liner for both the web request and deserialization. The <code>MajorRelease<\/code> type is provided, via the generic method, as the type of object to return from the JSON deserialization process.<\/li>\n<li>The <code>?? throw new Exception()<\/code> <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/operators\/null-coalescing-operator\">null-coalescing operator<\/a> is a sort of &#8220;last resort&#8221; option in case the network call returns a null response and plays nicely with <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/nullable-references\">non-nullable reference types<\/a>.<\/li>\n<li>The <code>GetVersion<\/code> method produces a <code>Version<\/code> object that is added to an implicit <code>List&lt;Version&gt;<\/code> via a <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/whats-new\/csharp-12#collection-expressions\">collection expression<\/a> in the <code>Report<\/code> (<code>record<\/code>) constructor.<\/li>\n<\/ul>\n<p>Deeper into the implementation, you&#8217;ll find an iterator method (that includes <code>yield return<\/code>). I think of iterator methods as &#8220;programmable <code>IEnumerable&lt;T&gt;<\/code> machines&#8221; and that is in fact exactly what they are. I love using them because they present a nice API while allowing me to offload a bunch of clutter from more focused methods.<\/p>\n<p>The following iterator method is doing the heavy lifting in this implementation. It finds the first and first patch release for the compliance report.<\/p>\n<pre><code class=\"language-csharp\">\/\/ Get first and first security release\npublic static IEnumerable&lt;ReportJson.PatchRelease&gt; GetReleases(MajorRelease majorRelease)\n{\n    bool securityOnly = false;\n\n    foreach (ReleaseJson.Release release in majorRelease.Releases)\n    {\n        if (securityOnly &amp;&amp; !release.Security)\n        {\n            continue;\n        }\n\n        yield return new(release.ReleaseDate, GetDaysAgo(release.ReleaseDate, true), release.ReleaseVersion, release.Security, release.CveList);\n\n        if (release.Security)\n        {\n            yield break;\n        }\n        else if (!securityOnly)\n        {\n            securityOnly = true;\n        }\n    }\n\n    yield break;\n}<\/code><\/pre>\n<p>The <code>GetVersion<\/code> <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/methods#expression-bodied-members\">expression-body method<\/a> calls <code>ToList<\/code> on the <code>GetReleases<\/code> iterator method, to create a <code>List&lt;Release&gt;<\/code> as part of the instantiation of the <code>Version<\/code> record. It results in very tight syntax. I&#8217;m also using patterns, which I equally love.<\/p>\n<pre><code class=\"language-csharp\">public static MajorVersion GetVersion(MajorRelease release) =&gt;\n    new(release.ChannelVersion, \n        release.SupportPhase is \"active\" or \"maintainence\", \n        release.EolDate ?? \"\", \n        release.EolDate is null ? 0 : GetDaysAgo(release.EolDate), \n        GetReleases(release).ToList()\n        );<\/code><\/pre>\n<p>That&#8217;s almost the entirety of the implementation. The <code>GetDaysAgo<\/code> method and the record definitions are the only other pieces you&#8217;ll need to look at the implementation to see.<\/p>\n<p>Source generation is straightforward to enable. The only difference is that the two calls to the serializer include static property values from <code>*Context*<\/code> types, coming from two partial class declarations. That&#8217;s it!<\/p>\n<pre><code class=\"language-csharp\">public static async Task&lt;int&gt; MakeReportWebAsync(string url)\n{\n    using HttpClient httpClient= new();\n    var release = await httpClient.GetFromJsonAsync(url, ReleaseRecordContext.Default.MajorRelease) ?? throw new Exception(Error.BADJSON);\n    Report report = new(DateTime.Today.ToShortDateString(), [ GetVersion(release) ]);\n    string reportJson = JsonSerializer.Serialize(report, ReportRecordContext.Default.Report);\n    WriteJsonToConsole(reportJson);\n    return reportJson.Length;\n}\n\n[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.KebabCaseLower)]\n[JsonSerializable(typeof(MajorRelease))]\npublic partial class ReleaseRecordContext : JsonSerializerContext\n{\n}\n\n[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.KebabCaseLower)]\n[JsonSerializable(typeof(Report))]\npublic partial class ReportRecordContext : JsonSerializerContext\n{\n}<\/code><\/pre>\n<p>You can see that I&#8217;ve opted into using <code>JsonKnownNamingPolicy.KebabCaseLower<\/code>, since that matches the <code>releases.json<\/code> schema. In my opinion, it&#8217;s even more convenient to specify <code>JsonSerializerOptions<\/code> with source generation than without.<\/p>\n<blockquote>\n<p>Bottom line: <code>JsonSerializer<\/code> makes code very compact and easy to reason about. The serializer is quite efficient. It is your default option for reading and writing JSON documents.<\/p>\n<\/blockquote>\n<h2>Json.NET Serializer<\/h2>\n<p>The Json.NET <code>JsonSerializer<\/code> is very similar in terms of approachability as <code>System.Text.Json<\/code>. In fact, <code>System.Text.Json<\/code> was modeled on <code>Newtonsoft.Json<\/code> so this should be no surprise.<\/p>\n<p>Implementation:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/releasejson\/NewtonsoftJsonSerializerBenchmark.cs\"><code>NewtonsoftJsonSerializerBenchmark<\/code><\/a><\/li>\n<\/ul>\n<p>The primary part of the implementation is also quite compact. It&#8217;s almost identical to the previous code except that there isn&#8217;t a handy extension method for <code>HttpClient<\/code>.<\/p>\n<pre><code class=\"language-csharp\">public static async Task&lt;int&gt; MakeReportWebAsync(string url)\n{\n    \/\/ Make network call\n    using var httpClient = new HttpClient();\n    using var releaseMessage = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);\n    using var stream = await releaseMessage.Content.ReadAsStreamAsync();\n\n    \/\/ Attach stream to serializer\n    JsonSerializer serializer = new();\n    using StreamReader sr = new(stream);\n    using JsonReader reader = new JsonTextReader(sr);\n\n    \/\/ Process JSON\n    MajorRelease release = serializer.Deserialize&lt;MajorRelease&gt;(reader) ?? throw new Exception(Error.BADJSON);\n    Report report = new(DateTime.Today.ToShortDateString(), [ GetVersion(release) ]);\n    string reportJson = JsonConvert.SerializeObject(report);\n    WriteJsonToConsole(reportJson);\n    return reportJson.Length;\n}<\/code><\/pre>\n<p>There isn&#8217;t a lot of extra analysis to provide. From a coding standpoint, at least for this app, it&#8217;s the same (enough) as the <code>System.Text.Json<\/code> version. The rest of the code in the implementation is actually identical, so no need to look at that again.<\/p>\n<p>This code has several <code>using<\/code> statements, which are used to ensure that resources are properly disposed after the method exits. You can sometimes get away without doing that, but it is good practice.<\/p>\n<p>We should take a quick look at <code>httpClient.GetAsync(JsonBenchmark.Url, HttpCompletionOption.ResponseHeadersRead)<\/code>. It will show up in all the remaining examples. The <code>HttpCompletionOption.ResponseHeadersRead<\/code> enum value is telling <code>HttpClient<\/code> to only eagerly read the HTTP headers in the response and then wait for the reader (in this case <code>JsonSerializer<\/code>) to request more bytes from the server as it can read them. This pattern can be important to avoid memory spikes.<\/p>\n<p>Json.NET doesn&#8217;t provide a source generator option. It&#8217;s also not compatible with trimming and native AOT as a result.<\/p>\n<blockquote>\n<p>Bottom line: The Json.NET <code>JsonSerializer<\/code> is an excellent JSON implementation and has served millions of .NET developers well for many years. If you are happy with it, you should continue using it.<\/p>\n<\/blockquote>\n<h2>JsonNode<\/h2>\n<p><code>JsonNode<\/code> is a typical document object model API that both provides an alternate API for the JSON type system (using <code>JsonObject<\/code>, <code>JsonArray<\/code>, and <code>JsonValue<\/code> to represent JSON objects, arrays and primitive values respectively) while integrating with the .NET type system as much as possible (<code>JsonArray<\/code> is an <code>IEnumerable<\/code>). Most of the API is oriented on a dictionary key-value style syntax.<\/p>\n<p>Implementation:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/releasejson\/JsonNodeBenchmark.cs\"><code>JsonNodeBenchmark<\/code><\/a><\/li>\n<\/ul>\n<p>This coding pattern is quite different. It seems like 3x the code, but that&#8217;s because I&#8217;ve chosen to include much more of the actual implementation in the primary method. Given the DOM paradigm, I think this makes sense. In actuality, this code is pretty compact given that we&#8217;re starting to do the heavy lifting of serialization ourselves. It&#8217;s also straightforward to read, particularly with the nested <code>report<\/code> code.<\/p>\n<pre><code class=\"language-csharp\">public static async Task&lt;int&gt; MakeReportAsync(string url)\n{\n    \/\/ Make network call\n    var httpClient = new HttpClient();\n    using var responseMessage = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);\n    var stream = await responseMessage.Content.ReadAsStreamAsync();\n\n    \/\/ Parse Json from stream\n    var doc = await JsonNode.ParseAsync(stream) ?? throw new Exception(Error.BADJSON);\n    var version = doc[\"channel-version\"]?.ToString() ?? \"\";\n    var supported = doc[\"support-phase\"]?.ToString() is \"active\" or \"maintenance\";\n    var eolDate = doc[\"eol-date\"]?.ToString() ??  \"\";\n    var releases = doc[\"releases\"]?.AsArray() ?? [];\n\n    \/\/ Generate report\n    var report = new JsonObject()\n    {\n        [\"report-date\"] = DateTime.Now.ToShortDateString(),\n        [\"versions\"] = new JsonArray()\n        {\n            new JsonObject()\n            {\n                [\"version\"] = version,\n                [\"supported\"] = supported,\n                [\"eol-date\"] = eolDate,\n                [\"support-ends-in-days\"] = eolDate is null ? null : GetDaysAgo(eolDate, true),\n                [\"releases\"] = GetReportForReleases(releases),\n            }\n        }\n    };\n\n    \/\/ Generate JSON\n    string reportJson = report.ToJsonString(_options);\n    WriteJsonToConsole(reportJson);\n    return reportJson.Length;\n}<\/code><\/pre>\n<p>The rest of the code has much the same pattern. <code>GetReportForReleases<\/code> is primarily a <code>foreach<\/code> over <code>doc[\"releases\"]<\/code>. As mentioned earlier, a <code>JsonArray<\/code> exposes an <code>IEnumerable&lt;JsonNode&gt;<\/code>, making it natural to integrate with idiomatic C# syntax. You could just as easily use <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/programming-guide\/concepts\/linq\/introduction-to-linq-queries\">LINQ<\/a> with <code>JsonArray<\/code>.<\/p>\n<p>There are a few highlights to call out with this implementation. <\/p>\n<ul>\n<li><code>JsonNode<\/code> isn&#8217;t integrated with <code>HttpClient<\/code> in the same way as <code>JsonSerializer<\/code> but it doesn&#8217;t really matter. <code>JsonNode.ParseAsync<\/code> is happy to accept a <code>Stream<\/code> from <code>HttpClient<\/code> and there is no one-liner that makes sense for <code>JsonNode<\/code>.<\/li>\n<li>The DOM API can return <code>null<\/code> from a key-value request, like from <code>doc[\"not-a-propertyname-in-this-schema\"]<\/code>.<\/li>\n<li>Generating JSON with <code>JsonNode<\/code> is delightful since you can use types and C# expressions while visualizing a nesting pattern that almost looks like JSON (if you squint). <\/li>\n<\/ul>\n<blockquote>\n<p>Bottom line: <code>JsonNode<\/code> is a great API if you like the DOM access pattern or cannot generate types needed to use a serializer. It is your default choice if you want the absolute quickest path to programmatically read and write JSON. It is ideally suited if you want to read, manipulate and write JSON documents. For read-only querying of JSON text you might want to consider using the faster <code>JsonDocument<\/code> instead.<\/p>\n<\/blockquote>\n<h2>Utf8JsonReader<\/h2>\n<p><code>Utf8JsonReader<\/code> is our &#8220;manual mode&#8221; solution. If you are up for it, there&#8217;s a lot of capability and power on offer. Like I said earlier, the higher-level <code>System.Text.Json<\/code> APIs are built on top of this type, so its clear that you can build very useful JSON processing algorithms with it.<\/p>\n<p>Implementations:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/releasejson\/Utf8JsonReaderWriterStreamBenchmark.cs\"><code>Utf8JsonReaderWriterStreamBenchmark<\/code><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/releasejson\/Utf8JsonReaderWriterPipelineBenchmark.cs\"><code>Utf8JsonReaderWriterPipelineBenchmark<\/code><\/a><\/li>\n<li><a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/releasejson\/Utf8JsonReaderWriterStreamRawBenchmark.cs\"><code>Utf8JsonReaderWriterStreamRawBenchmark<\/code><\/a><\/li>\n<\/ul>\n<p>Again, I wrote a few different implementations. The first two are identical except one uses <code>Stream<\/code> and the other uses <code>Pipelines<\/code>. They both deserialize JSON to real objects, much like a serializer would do. The objects are then handed to some JSON writer code, which serializes those objects to JSON, much like a serializer would do. I like this approach a lot and it feels pretty natural (for low-level code). When I next need more control over processing JSON, this is the approach I&#8217;ll use. In retrospect, I could have used the serializer as part of this approach, with <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.text.json.jsonserializer.deserialize\">JsonSerializer.Deserialize<\/a> (passing a <code>Utf8JsonReader<\/code>) at least as a test.<\/p>\n<p>Pipelines offer a higher-level API than streams, which makes the buffer management more approachable. The <code>Stream<\/code> code required a little more careful handling than I enjoy needing to deal with.<\/p>\n<p>The third implementation copies UTF8 data directly from the reader to the writer without creating intermediate objects. That approach is more efficient and has a significantly lower line count because of that. However, the code isn&#8217;t layered as much, which I suspect would hurt maintenance. I&#8217;m less likely to return to this pattern.<\/p>\n<p>These implementations are different enough that we should look at results again, for these three approaches, using <code>JsonSerializer<\/code> as the baseline.<\/p>\n<p><img decoding=\"async\" title=\"Lines of code using UTF8JsonReader\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/json-api-loc-utf8.png\" width=\"75%\" \/><\/p>\n<p>The &#8220;raw&#8221; implementation pulls ahead of the other <code>Utf8JsonReader<\/code> implementations. The <code>JsonSerializer<\/code> implementation continues to stand as a great baseline for straightforward code. <\/p>\n<p>Let&#8217;s look at the performance of these three implementations.<\/p>\n<p><img decoding=\"async\" title=\"Utf8JsonReader performance results for large JSON file on MacBook M1\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2023\/10\/utf8json-api-speed-large-document-local.png\" width=\"75%\" \/><\/p>\n<p>These are measured by reading the large JSON document from the local file system on my MacBook M1. The &#8220;Raw&#8221; approach was consistently faster (not just in this specific benchmark).    <\/p>\n<p>The memory differences between the implementations were even tighter, so I&#8217;m not going to bother sharing those again. I materialized some of the JSON strings to UTF16 strings in my &#8220;raw&#8217; implementation. It is possible to not do that for lower memory usage, but I didn&#8217;t explore that.<\/p>\n<p>Let&#8217;s move onto the code.<\/p>\n<p>This coding pattern (from the first implementation) is different again, with more complex topics and techniques, although much of the lower-level API use is hidden in the implementation of the types in the <code>\/\/ Process JSON<\/code> section.<\/p>\n<pre><code class=\"language-csharp\">public static async Task&lt;int&gt; MakeReportWebAsync(string url)\n{\n    \/\/ Make network call\n    using var httpClient = new HttpClient();\n    using var releaseMessage = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);\n    releaseMessage.EnsureSuccessStatusCode();\n    using var jsonStream = await releaseMessage.Content.ReadAsStreamAsync();\n\n    \/\/ Acquire byte[] as a buffer for the Stream \n    byte[] rentedArray = ArrayPool&lt;byte&gt;.Shared.Rent(JsonStreamReader.Size);\n    int read = await jsonStream.ReadAsync(rentedArray);\n\n    \/\/ Process JSON\n    var releasesReader = new ReleasesJsonReader(new(jsonStream, rentedArray, read));\n    var memory = new MemoryStream();\n    var reportWriter = new ReportJsonWriter(releasesReader, memory);\n    await reportWriter.Write();\n    ArrayPool&lt;byte&gt;.Shared.Return(rentedArray);\n\n    \/\/ Flush stream and prepare for reader\n    memory.Flush();\n    memory.Position= 0;\n\n    WriteJsonToConsole(memory);\n    return (int)memory.Length;\n}<\/code><\/pre>\n<p>There are a few highlights to call out.<\/p>\n<ul>\n<li><code>ArrayPool<\/code> is a pattern for re-using arrays, which significantly reduces the work of the garbage collector. It is a great choice if you are repeatedly reading JSON files (like in each web request or performance measurement).<\/li>\n<li>I had to build a little mini framework for <code>Utf8JsonReader<\/code> JSON processing. I wrote a general <code>JsonStreamReader<\/code> on top of <code>Utf8JsonReader<\/code> and then a schema-specific <code>ReleasesJsonReader<\/code> on top of that. I did the same thing with the pipelines implementation, with <code>JsonPipelineReader<\/code>. This approach and structure made the code a lot easier to write.<\/li>\n<li>The final result is JSON that is written to a <code>MemoryStream<\/code>. That stream needs to be flushed and its position reset in order to be read by a reader.<\/li>\n<\/ul>\n<p>The <code>ReportJsonWriter<\/code> shows how the overall code flows.<\/p>\n<pre><code class=\"language-csharp\">public async Task Write()\n{\n    _writer.WriteStartObject();\n    _writer.WriteString(\"report-date\"u8, DateTime.Now.ToShortDateString());\n    _writer.WriteStartArray(\"versions\"u8);\n\n    var version = await _reader.GetVersionAsync();\n    WriteVersionObject(version);\n    _writer.WritePropertyName(\"releases\"u8);\n    \/\/ Start releases\n    _writer.WriteStartArray();\n\n    await foreach (var release in _reader.GetReleasesAsync())\n    {\n        WriteReleaseObject(release);\n    }\n\n    \/\/ End releases\n    _writer.WriteEndArray();\n\n    \/\/ End JSON document\n    _writer.WriteEndObject();\n    _writer.WriteEndArray();\n    _writer.WriteEndObject();\n\n    \/\/ Write content\n    _writer.Flush();\n}<\/code><\/pre>\n<p><code>_writer<\/code> (<code>Utf8JsonWriter<\/code>) writes JSON to the <code>MemoryStream<\/code>, while <code>_reader<\/code> (<code>ReleasesJsonReader<\/code>) exposes APIs for reading objects from the source JSON. <code>ReleasesJsonReader<\/code> is a forward-only JSON reader so the APIs can only be called in a certain order (and they throw otherwise). A <code>ParseState<\/code> enum is used to record processing state through the document for that purpose.<\/p>\n<p>The most interesting part is that the <code>ReleasesJsonReader<\/code> APIs are async, yet <code>Utf8JsonReader<\/code> doesn&#8217;t support being used within async methods because it is a <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/builtin-types\/ref-struct\">ref struct<\/a>. The following block of code does a good job of demonstrating a pattern for using <code>Utf8JsonReader<\/code> within async workflows and what <code>Utf8JsonReader<\/code> code looks like generally.<\/p>\n<pre><code class=\"language-csharp\">private async IAsyncEnumerable&lt;Cve&gt; GetCvesAsync()\n{\n    while (!_json.ReadToTokenType(JsonTokenType.EndArray, false))\n    {\n        await _json.AdvanceAsync();\n    }\n\n    while(GetCve(out Cve? cve))\n    {\n        yield return cve;\n    }\n\n    yield break;\n}\n\nprivate bool GetCve([NotNullWhen(returnValue:true)] out Cve? cve)\n{\n    string? cveId = null;\n    cve = null;\n\n    var reader = _json.GetReader();\n\n    while (true)\n    {\n        reader.Read();\n\n        if (reader.TokenType is JsonTokenType.EndArray)\n        {\n            return false;\n        }\n        else if (!reader.IsProperty())\n        {\n            continue;\n        }\n        else if (reader.ValueTextEquals(\"cve-id\"u8))\n        {\n            reader.Read();\n            cveId = reader.GetString();\n        }\n        else if (reader.ValueTextEquals(\"cve-url\"u8))\n        {\n            reader.Read();\n            var cveUrl = reader.GetString();\n\n            if (string.IsNullOrEmpty(cveUrl) ||\n                string.IsNullOrEmpty(cveId))\n            {                    \n                throw new Exception(BenchmarkData.BADJSON);\n            }\n\n            cve = new Cve(cveId, cveUrl);\n            reader.Read();\n            _json.UpdateState(reader);\n            return true;\n        }\n\n    }\n}<\/code><\/pre>\n<p>Note: <a href=\"https:\/\/www.cve.org\/\">CVEs<\/a> are part of the <code>releases.json<\/code> schema. CVEs can be described as &#8220;documented security vulnerability&#8221;.<\/p>\n<p>I&#8217;ll share the highlights on what&#8217;s going on across these two methods.<\/p>\n<ul>\n<li><code>GetCvesAsync<\/code> is an async iterator-style method. As I said earlier, I use iterator methods whenever I can. You can use them for <code>IAsyncEnumerable&lt;T&gt;<\/code>, just like <code>IEnumerable&lt;T&gt;<\/code>.<\/li>\n<li>The method starts with a call to <code>JsonStreamReader.ReadToTokenType<\/code>. This is asking the underlying <code>JsonStreamReader<\/code> to read to a <code>JsonTokenType.EndArray<\/code> token (the end of the <code>cve-list<\/code> array) within the buffer. If not, then <code>JsonStreamReader.AdvanceAsync<\/code> is called to refresh the buffer.<\/li>\n<li>The <code>false<\/code> parameter indicates that the underlying reader state should not be updated after the <code>ReadToTokenType<\/code> operation completes. This pattern double-reads the JSON content to ensure that the end of the <code>cve-list<\/code> can be reached and that every <code>Utf8Json.Read<\/code> call in the <code>GetCve<\/code> method will return <code>true<\/code>. Double reading may sound bad, but it is better than alternative patterns. Also, this code is more network- than CPU-bound (as we observed in the performance data).<\/li>\n<li><code>GetCve<\/code> is not an async method, so we&#8217;re free to use <code>Utf8JsonReader<\/code>.<\/li>\n<li>The first thing we need to do is get a fresh <code>Utf8JsonReader<\/code> from state that has been previously saved.<\/li>\n<li>The remainder of the method is a state machine for processing the <code>cve-list<\/code> content, which is an array of JSON objects with two properies.<\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/proposals\/csharp-11.0\/utf8-string-literals\">UTF8 string literals<\/a> &#8212; like <code>\"cve-id\"u8<\/code> &#8212; are a very useful pattern used throughout the codebase. <a href=\"https:\/\/learn.microsoft.com\/dotnet\/csharp\/language-reference\/operators\/stackalloc\">Stack allocated<\/a> UTF8 strings are exposed as <code>ReadOnlySpan&lt;byte&gt;<\/code>, which is perfect for comparing to the UTF8 values exposed from <code>Utf8JsonReader<\/code>.<\/li>\n<li><code>JsonStreamReader.UpdateState<\/code> saves off the state of the <code>Utf8JsonReader<\/code> so that it can be recreated when it is next needed. Again, the reader cannot be stored in some instance field because it is a <code>ref struct<\/code>.<\/li>\n<li><code>[NotNullWhen(returnValue:true)]<\/code> is a helpful attribute for communicating when an <code>out<\/code> value can be trusted to be non-null.<\/li>\n<\/ul>\n<p>Why all this focus on ref structs and why did the team make this design choice? <code>Utf8JsonReader<\/code> uses <code>ReadOnlySpan&lt;T&gt;<\/code> pervasively throughout its implementation, which has the (large) benefit of avoiding copying JSON data. <code>ReadOnlySpan&lt;T&gt;<\/code> is a <code>ref struct<\/code> so therefore <code>Utf8JsonReader<\/code> must be as well. For example, a JSON string &#8212; like from <code>Utf8JsonRead.ValueSpan<\/code> &#8212; is a low-cost <code>ReadOnlySpan&lt;byte&gt;<\/code> pointing into the <code>rentedArray<\/code> buffer created at the start of the program. This design choice requires a little extra care to use, but is worth it for the performance value it delivers. Also, this extra complexity is hidden from view for <code>JsonSerializer<\/code> and <code>JsonNode<\/code> users. It&#8217;s only developers directly using <code>Utf8JsonReader<\/code> that need to care.<\/p>\n<p>To be clear, using <code>ReadOnlySpan&lt;T&gt;<\/code> doesn&#8217;t force a type to become a <code>ref struct<\/code>. The line where that happens is when you need to <a href=\"https:\/\/github.com\/dotnet\/runtime\/blob\/f08cbb88a074f5fd5c1b31751f44baa8c1d91cd6\/src\/libraries\/System.Text.Json\/src\/System\/Text\/Json\/Reader\/Utf8JsonReader.cs#L24\">store a ref struct as a (typically private) field<\/a>. <code>Utf8JsonReader<\/code> does that, hence <code>ref struct<\/code>.<\/p>\n<p>Last, the writer code looks like the following.<\/p>\n<pre><code class=\"language-csharp\">public void WriteVersionObject(Version version)\n{\n    _writer.WriteStartObject();\n    _writer.WritePropertyName(\"version\"u8);\n    _writer.WriteStringValue(version.MajorVersion);\n    _writer.WritePropertyName(\"supported\"u8);\n    _writer.WriteBooleanValue(version.Supported);\n    _writer.WritePropertyName(\"eol-date\"u8);\n    _writer.WriteStringValue(version.EolDate);\n    _writer.WritePropertyName(\"support-ends-in-days\"u8);\n    _writer.WriteNumberValue(version.SupportEndsInDays);\n}<\/code><\/pre>\n<p>This code is writing basic JSON content and taking advantage of UTF8 string literals (for the property names) to do that efficiently.<\/p>\n<p>The rest of the codebase follows these same patterns.<\/p>\n<p>I&#8217;ve skipped how <a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/releasejson\/JsonStreamReader.cs\"><code>JsonStreamReader<\/code><\/a> is implemented. It includes a set of <code>ReadTo<\/code> methods like <code>ReadToTokenType<\/code> and buffer management. It is indeed interesting, but I&#8217;ll leave that for folks to read themselves. This is a type that feels like it should be in available in a library for folks to simply rely on. Same thing with <a href=\"https:\/\/github.com\/richlander\/convenience\/blob\/main\/releasejson\/releasejson\/JsonPipeReader.cs\"><code>JsonPipeReader<\/code><\/a>. If you want to use pipelines, take a look at <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/87984\">dotnet\/runtime #87984<\/a>.<\/p>\n<blockquote>\n<p>Bottom line: <code>Utf8JsonReader<\/code> is a great and capable API and the rest of the <code>System.Text.Json<\/code> stack is built on top of it. In certain scenarios, it is the best choice since it offers maxiumum flexiblity. It requires a higher level of skill to navigate the interaction with lower-level APIs and concepts. If you have the need and the skill, this type can deliver.<\/p>\n<\/blockquote>\n<h2>Summary<\/h2>\n<p>The purpose of this post was to demonstrate that <code>System.Text.Json<\/code> offers JSON reading and writing APIs for every developer and scenario. The APIs cover the spectrum of convenience to control, both in terms of the coding patterns and the performane you can achieve.<\/p>\n<p>The punchline of the post is that the convenient option &#8212; <code>JsonSerializer<\/code> &#8212; delivers great performance and that it is competitive in all the scenarios I tested. That&#8217;s a great result.<\/p>\n<p>As I mentioned, <code>System.Text.Json<\/code> is a relatively new API family, <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/try-the-new-system-text-json-apis\/\">created as part of the .NET Core project<\/a>. It does a good job showing that a fully-integrated approach to platform design can produce a lot of benefits for usability and performance.<\/p>\n<p><code>Newtonsoft.Json<\/code> remains a great choice. As I said in the post, if it&#8217;s working for you, keep on using it.<\/p>\n<p>Thanks to <a href=\"https:\/\/github.com\/eiriktsarpalis\/\">Eirik Tsarpalis<\/a> for his help on this post. Thanks to <a href=\"https:\/\/github.com\/davidfowl\">David Fowler<\/a>, <a href=\"https:\/\/github.com\/jkotas\">Jan Kotas<\/a>, and <a href=\"https:\/\/github.com\/stephentoub\">Stephen Toub<\/a> for their help contributing to this series.<\/p>\n<p>Did you like this style of post? If you do, we&#8217;ll write more. It is intended to show you more than the latest features in the new release, but a practical application of how .NET APIs can be used to satisfy the needs of concrete tasks.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>JSON is one of the most common formats in apps today and .NET has great APIs for reading and writing JSON documents. It&#8217;s a great example of the convenience of .NET.<\/p>\n","protected":false},"author":1312,"featured_media":48233,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7590,3009],"tags":[7755,7223],"class_list":["post-48122","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-json","category-performance","tag-convenience-of-dotnet","tag-system-text-json"],"acf":[],"blog_post_summary":"<p>JSON is one of the most common formats in apps today and .NET has great APIs for reading and writing JSON documents. It&#8217;s a great example of the convenience of .NET.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/48122","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/1312"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=48122"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/48122\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/48233"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=48122"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=48122"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=48122"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}