Today I was writing a tool that needed to handle JSON, the content was relatively large and I only needed to pick out a few parts of the structure, and thought to myself this would be the perfect time to try out the new System.Text.Json JsonDocument.

To be clear, there are multiple APIs offered in System.Texts.Json from high level to low level, and JsonDocument sits somewhere in the middle, in most cases the simpler JsonSerializer APIs will be perfectly adequate and most appropriate.

The benefits of using JsonDocument are:

  • It has asynchronous APIs, for example JsonDocument.ParseAsync which takes a Stream, and importantly will call ReadAsync on that underlying stream.
  • It uses very little memory, is does this by:
    • Using shared rented buffers under the hood when reading the underlying stream.
    • Reading UTF8 content directly, and not having to convert UTF8 bytes into strings.
    • Only allocating when you ask it for a materialized value.

For my console application, these aren’t properties I particularly need, but for a backend service, web app or microservice, you start to really care about these concerns.

Getting Started

If you are targeting netcoreapp3.0, then there’s nothing you need to add to your project, just start using the System.Text.Json namespace.

For netstandard2.0 projects or full framework, you’ll need to add the following package:

<PackageReference Include="System.Text.Json" Version="4.6.0" />

The Problem

I want to parse the dotnet metadata file releases-index.json, found here and pick out the url for the channel JSON (2.2 for example). The channel JSON is much larger, I want to then search for a particular version and select the files for that release, you can see an example here.

The Code

var
(class) string
Represents text as a sequence of UTF-16 code units.
version
(local variable) string? version
= "2.2.100";
var
(class) string
Represents text as a sequence of UTF-16 code units.
channelVersion
(local variable) string? channelVersion
= ParseChannelVersion
(method) string ParseChannelVersion(string v)
(version
(local variable) string? version
); // "2.2"
var
(class) HttpClient
Provides a class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.
httpClient
(local variable) HttpClient? httpClient
= new HttpClient
(class) HttpClient
Provides a class for sending HTTP requests and receiving HTTP responses from a resource identified by a URI.
();
using var
(class) JsonDocument
Provides a mechanism for examining the structural content of a JSON value without automatically instantiating data values.
releasesResponse
(local variable) JsonDocument? releasesResponse
= await JsonDocument
(class) JsonDocument
Provides a mechanism for examining the structural content of a JSON value without automatically instantiating data values.
.ParseAsync
(method) Task<JsonDocument> JsonDocument.ParseAsync(Stream utf8Json, JsonDocumentOptions options = default(JsonDocumentOptions), CancellationToken cancellationToken = default(CancellationToken))
Parses a Stream as UTF-8-encoded data representing a single JSON value into a JsonDocument. The stream is read to completion.
utf8Json — The JSON data to parse.
options — Options to control the reader behavior during parsing.
cancellationToken — The token to monitor for cancellation requests.
A task to produce a JsonDocument representation of the JSON value.
JsonException — utf8Json does not represent a valid single JSON value.
ArgumentException — options contains unsupported options.
(await httpClient
(local variable) HttpClient? httpClient
.GetStreamAsync
(method) Task<Stream> HttpClient.GetStreamAsync(string? requestUri)
Send a GET request to the specified Uri and return the response body as a stream in an asynchronous operation.
requestUri — The Uri the request is sent to.
The task object representing the asynchronous operation.
InvalidOperationException — The requestUri must be an absolute URI or BaseAddress must be set.
HttpRequestException — The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation (or timeout for .NET Framework only).
TaskCanceledException — .NET Core and .NET 5 and later only: The request failed due to timeout.
UriFormatException — The provided request URI is not valid relative or absolute URI.
(
"https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases-index.json"));
var
(struct) JsonElement
Represents a specific JSON value within a JsonDocument.
channel
(local variable) JsonElement channel
= releasesResponse
(local variable) JsonDocument? releasesResponse
.RootElement
(property) JsonElement JsonDocument.RootElement
Gets the root element of this JSON document.
A JsonElement representing the value of the document.
.GetProperty
(method) JsonElement JsonElement.GetProperty(string propertyName)
Gets a JsonElement representing the value of a required property identified by propertyName.
propertyName — The name of the property whose value is to be returned.
A JsonElement representing the value of the requested property.
InvalidOperationException — This value's ValueKind is not Object.
KeyNotFoundException — No property was found with the requested name.
ArgumentNullException — propertyName is .
ObjectDisposedException — The parent JsonDocument has been disposed.
("releases-index").EnumerateArray
(method) JsonElement.ArrayEnumerator JsonElement.EnumerateArray()
Gets an enumerator to enumerate the values in the JSON array represented by this JsonElement.
An enumerator to enumerate the values in the JSON array represented by this JsonElement.
InvalidOperationException — This value's ValueKind is not Array.
ObjectDisposedException — The parent JsonDocument has been disposed.
()
.First
(extension) JsonElement IEnumerable<JsonElement>.First<JsonElement>(Func<JsonElement, bool> predicate)
Returns the first element in a sequence that satisfies a specified condition.
source — An IEnumerable`1 to return an element from.
predicate — A function to test each element for a condition.
The first element in the sequence that passes the test in the specified predicate function.
ArgumentNullException — source or predicate is .
InvalidOperationException — No element satisfies the condition in predicate. -or- The source sequence is empty.
(x
(parameter) JsonElement x
=>
(extension) JsonElement IEnumerable<JsonElement>.First<JsonElement>(Func<JsonElement, bool> predicate)
Returns the first element in a sequence that satisfies a specified condition.
source — An IEnumerable`1 to return an element from.
predicate — A function to test each element for a condition.
The first element in the sequence that passes the test in the specified predicate function.
ArgumentNullException — source or predicate is .
InvalidOperationException — No element satisfies the condition in predicate. -or- The source sequence is empty.
x
(parameter) JsonElement x
.GetProperty
(method) JsonElement JsonElement.GetProperty(string propertyName)
Gets a JsonElement representing the value of a required property identified by propertyName.
propertyName — The name of the property whose value is to be returned.
A JsonElement representing the value of the requested property.
InvalidOperationException — This value's ValueKind is not Object.
KeyNotFoundException — No property was found with the requested name.
ArgumentNullException — propertyName is .
ObjectDisposedException — The parent JsonDocument has been disposed.
("channel-version").GetString
(method) string? JsonElement.GetString()
Gets the value of the element as a String.
The value of the element as a String.
InvalidOperationException — This value's ValueKind is neither String nor Null.
ObjectDisposedException — The parent JsonDocument has been disposed.
() ==
(extension) JsonElement IEnumerable<JsonElement>.First<JsonElement>(Func<JsonElement, bool> predicate)
Returns the first element in a sequence that satisfies a specified condition.
source — An IEnumerable`1 to return an element from.
predicate — A function to test each element for a condition.
The first element in the sequence that passes the test in the specified predicate function.
ArgumentNullException — source or predicate is .
InvalidOperationException — No element satisfies the condition in predicate. -or- The source sequence is empty.
channelVersion
(local variable) string? channelVersion
);
var
(class) string
Represents text as a sequence of UTF-16 code units.
channelJson
(local variable) string? channelJson
= channel
(local variable) JsonElement channel
.GetProperty
(method) JsonElement JsonElement.GetProperty(string propertyName)
Gets a JsonElement representing the value of a required property identified by propertyName.
propertyName — The name of the property whose value is to be returned.
A JsonElement representing the value of the requested property.
InvalidOperationException — This value's ValueKind is not Object.
KeyNotFoundException — No property was found with the requested name.
ArgumentNullException — propertyName is .
ObjectDisposedException — The parent JsonDocument has been disposed.
("releases.json").GetString
(method) string? JsonElement.GetString()
Gets the value of the element as a String.
The value of the element as a String.
InvalidOperationException — This value's ValueKind is neither String nor Null.
ObjectDisposedException — The parent JsonDocument has been disposed.
();
using var
(class) JsonDocument
Provides a mechanism for examining the structural content of a JSON value without automatically instantiating data values.
channelResponse
(local variable) JsonDocument? channelResponse
= await JsonDocument
(class) JsonDocument
Provides a mechanism for examining the structural content of a JSON value without automatically instantiating data values.
.ParseAsync
(method) Task<JsonDocument> JsonDocument.ParseAsync(Stream utf8Json, JsonDocumentOptions options = default(JsonDocumentOptions), CancellationToken cancellationToken = default(CancellationToken))
Parses a Stream as UTF-8-encoded data representing a single JSON value into a JsonDocument. The stream is read to completion.
utf8Json — The JSON data to parse.
options — Options to control the reader behavior during parsing.
cancellationToken — The token to monitor for cancellation requests.
A task to produce a JsonDocument representation of the JSON value.
JsonException — utf8Json does not represent a valid single JSON value.
ArgumentException — options contains unsupported options.
(await httpClient
(local variable) HttpClient? httpClient
.GetStreamAsync
(method) Task<Stream> HttpClient.GetStreamAsync(string? requestUri)
Send a GET request to the specified Uri and return the response body as a stream in an asynchronous operation.
requestUri — The Uri the request is sent to.
The task object representing the asynchronous operation.
InvalidOperationException — The requestUri must be an absolute URI or BaseAddress must be set.
HttpRequestException — The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation (or timeout for .NET Framework only).
TaskCanceledException — .NET Core and .NET 5 and later only: The request failed due to timeout.
UriFormatException — The provided request URI is not valid relative or absolute URI.
(channelJson
(local variable) string? channelJson
));
var
(class) string
Represents text as a sequence of UTF-16 code units.
files
(local variable) string? files
= channelResponse
(local variable) JsonDocument? channelResponse
.RootElement
(property) JsonElement JsonDocument.RootElement
Gets the root element of this JSON document.
A JsonElement representing the value of the document.
.GetProperty
(method) JsonElement JsonElement.GetProperty(string propertyName)
Gets a JsonElement representing the value of a required property identified by propertyName.
propertyName — The name of the property whose value is to be returned.
A JsonElement representing the value of the requested property.
InvalidOperationException — This value's ValueKind is not Object.
KeyNotFoundException — No property was found with the requested name.
ArgumentNullException — propertyName is .
ObjectDisposedException — The parent JsonDocument has been disposed.
("releases").EnumerateArray
(method) JsonElement.ArrayEnumerator JsonElement.EnumerateArray()
Gets an enumerator to enumerate the values in the JSON array represented by this JsonElement.
An enumerator to enumerate the values in the JSON array represented by this JsonElement.
InvalidOperationException — This value's ValueKind is not Array.
ObjectDisposedException — The parent JsonDocument has been disposed.
()
.SelectMany
(extension) IEnumerable<JsonElement> IEnumerable<JsonElement>.SelectMany<JsonElement, JsonElement>(Func<JsonElement, IEnumerable<JsonElement>> selector)
Projects each element of a sequence to an IEnumerable`1 and flattens the resulting sequences into one sequence.
source — A sequence of values to project.
selector — A transform function to apply to each element.
An IEnumerable`1 whose elements are the result of invoking the one-to-many transform function on each element of the input sequence.
ArgumentNullException — source or selector is .
(x
(parameter) JsonElement x
=>
(extension) IEnumerable<JsonElement> IEnumerable<JsonElement>.SelectMany<JsonElement, JsonElement>(Func<JsonElement, IEnumerable<JsonElement>> selector)
Projects each element of a sequence to an IEnumerable`1 and flattens the resulting sequences into one sequence.
source — A sequence of values to project.
selector — A transform function to apply to each element.
An IEnumerable`1 whose elements are the result of invoking the one-to-many transform function on each element of the input sequence.
ArgumentNullException — source or selector is .
{
IEnumerable
(interface) IEnumerable<JsonElement>
Exposes the enumerator, which supports a simple iteration over a collection of a specified type.
<
(interface) IEnumerable<JsonElement>
Exposes the enumerator, which supports a simple iteration over a collection of a specified type.
JsonElement
(struct) JsonElement
Represents a specific JSON value within a JsonDocument.
>
(interface) IEnumerable<JsonElement>
Exposes the enumerator, which supports a simple iteration over a collection of a specified type.
GetSdks
(method) IEnumerable<JsonElement> GetSdks()
()
{
yield return x
(parameter) JsonElement x
.GetProperty
(method) JsonElement JsonElement.GetProperty(string propertyName)
Gets a JsonElement representing the value of a required property identified by propertyName.
propertyName — The name of the property whose value is to be returned.
A JsonElement representing the value of the requested property.
InvalidOperationException — This value's ValueKind is not Object.
KeyNotFoundException — No property was found with the requested name.
ArgumentNullException — propertyName is .
ObjectDisposedException — The parent JsonDocument has been disposed.
("sdk");
if (x
(parameter) JsonElement x
.TryGetProperty
(method) bool JsonElement.TryGetProperty(string propertyName, JsonElement value)
Looks for a property named propertyName in the current object, returning a value that indicates whether or not such a property exists. When the property exists, its value is assigned to the value argument.
propertyName — The name of the property to find.
value — When this method returns, contains the value of the specified property.
if the property was found; otherwise, .
InvalidOperationException — This value's ValueKind is not Object.
ArgumentNullException — propertyName is .
ObjectDisposedException — The parent JsonDocument has been disposed.
("sdks", out var
(struct) JsonElement
Represents a specific JSON value within a JsonDocument.
sdks
(method) bool JsonElement.TryGetProperty(string propertyName, JsonElement value)
Looks for a property named propertyName in the current object, returning a value that indicates whether or not such a property exists. When the property exists, its value is assigned to the value argument.
propertyName — The name of the property to find.
value — When this method returns, contains the value of the specified property.
if the property was found; otherwise, .
InvalidOperationException — This value's ValueKind is not Object.
ArgumentNullException — propertyName is .
ObjectDisposedException — The parent JsonDocument has been disposed.
))
{
foreach (var
(struct) JsonElement
Represents a specific JSON value within a JsonDocument.
y
(method) IEnumerable<JsonElement> GetSdks()
in sdks
(local variable) JsonElement sdks
.EnumerateArray
(method) JsonElement.ArrayEnumerator JsonElement.EnumerateArray()
Gets an enumerator to enumerate the values in the JSON array represented by this JsonElement.
An enumerator to enumerate the values in the JSON array represented by this JsonElement.
InvalidOperationException — This value's ValueKind is not Array.
ObjectDisposedException — The parent JsonDocument has been disposed.
())
yield return y
(local variable) JsonElement y
;
}
}
return GetSdks
(method) IEnumerable<JsonElement> GetSdks()
();
})
.First
(extension) JsonElement IEnumerable<JsonElement>.First<JsonElement>(Func<JsonElement, bool> predicate)
Returns the first element in a sequence that satisfies a specified condition.
source — An IEnumerable`1 to return an element from.
predicate — A function to test each element for a condition.
The first element in the sequence that passes the test in the specified predicate function.
ArgumentNullException — source or predicate is .
InvalidOperationException — No element satisfies the condition in predicate. -or- The source sequence is empty.
(x
(parameter) JsonElement x
=>
(extension) JsonElement IEnumerable<JsonElement>.First<JsonElement>(Func<JsonElement, bool> predicate)
Returns the first element in a sequence that satisfies a specified condition.
source — An IEnumerable`1 to return an element from.
predicate — A function to test each element for a condition.
The first element in the sequence that passes the test in the specified predicate function.
ArgumentNullException — source or predicate is .
InvalidOperationException — No element satisfies the condition in predicate. -or- The source sequence is empty.
x
(parameter) JsonElement x
.GetProperty
(method) JsonElement JsonElement.GetProperty(string propertyName)
Gets a JsonElement representing the value of a required property identified by propertyName.
propertyName — The name of the property whose value is to be returned.
A JsonElement representing the value of the requested property.
InvalidOperationException — This value's ValueKind is not Object.
KeyNotFoundException — No property was found with the requested name.
ArgumentNullException — propertyName is .
ObjectDisposedException — The parent JsonDocument has been disposed.
("version").GetString
(method) string? JsonElement.GetString()
Gets the value of the element as a String.
The value of the element as a String.
InvalidOperationException — This value's ValueKind is neither String nor Null.
ObjectDisposedException — The parent JsonDocument has been disposed.
() ==
(extension) JsonElement IEnumerable<JsonElement>.First<JsonElement>(Func<JsonElement, bool> predicate)
Returns the first element in a sequence that satisfies a specified condition.
source — An IEnumerable`1 to return an element from.
predicate — A function to test each element for a condition.
The first element in the sequence that passes the test in the specified predicate function.
ArgumentNullException — source or predicate is .
InvalidOperationException — No element satisfies the condition in predicate. -or- The source sequence is empty.
version
(local variable) string? version
)
.GetProperty
(method) JsonElement JsonElement.GetProperty(string propertyName)
Gets a JsonElement representing the value of a required property identified by propertyName.
propertyName — The name of the property whose value is to be returned.
A JsonElement representing the value of the requested property.
InvalidOperationException — This value's ValueKind is not Object.
KeyNotFoundException — No property was found with the requested name.
ArgumentNullException — propertyName is .
ObjectDisposedException — The parent JsonDocument has been disposed.
("files").ToString
(method) string JsonElement.ToString()
Gets a string representation for the current value appropriate to the value type.
A string representation for the current value appropriate to the value type.
ObjectDisposedException — The parent JsonDocument has been disposed.
();

When creating the JsonDocument, it could have been tempting to write it as:

JsonDocument
(class) JsonDocument
Provides a mechanism for examining the structural content of a JSON value without automatically instantiating data values.
.Parse
(method) JsonDocument JsonDocument.Parse(string json, JsonDocumentOptions options = default(JsonDocumentOptions))
Parses text representing a single JSON string value into a JsonDocument.
json — The JSON text to parse.
options — Options to control the reader behavior during parsing.
A JsonDocument representation of the JSON value.
JsonException — json does not represent a valid single JSON value.
ArgumentException — options contains unsupported options.
(await httpClient
(local variable) HttpClient? httpClient
.GetStringAsync
(method) Task<string> HttpClient.GetStringAsync(string? requestUri)
Send a GET request to the specified Uri and return the response body as a string in an asynchronous operation.
requestUri — The Uri the request is sent to.
The task object representing the asynchronous operation.
InvalidOperationException — The requestUri must be an absolute URI or BaseAddress must be set.
HttpRequestException — The request failed due to an underlying issue such as network connectivity, DNS failure, server certificate validation (or timeout for .NET Framework only).
TaskCanceledException — .NET Core and .NET 5 and later only: The request failed due to timeout.
UriFormatException — The provided request URI is not valid relative or absolute URI.
("..."));

however this would have materialized the entire contents as a string first, undermining the overall benefits of using this API.

Also notice the use of using var releasesResponse here, this is a new C# 8.0 feature that means Dispose will be called at the end of the method, without any extra indentation that comes with the more familiar using block.

It’s important to always dispose of JsonDocument, as not doing so will result in the rented buffers never being returned to the pool, and you have a memory leak on your hands!

From then on the API is very easy to use, you can call .GetProperty("property-name").Get*() to get a typed value, and it’s only at this point that things will be heap allocated. Where we have optional properties, we use TryGetProperty, which will return a boolean indicated whether the property was found, and the value is passed as an out variable.

Conclusion

What’s really nice here is that I didn’t need to make a tradeoff between readability and performance, I “only paid for what I used”, and I was able to avoid the ceremony of creating a set of classes that comprehensively modelled the content which I was mostly discarding.

More information about System.Text.Json can be found here:

Try the new System.Text.Json APIs | .NET Blog
For .NET Core 3.0, we’re shipping a brand new namespace called System.Text.Json with support for a reader/writer, a document object model (DOM), and a serializer. In this blog post, I’m telling you why we built it, how it works, and how you can try it.