How to use a JSON document, Utf8JsonReader, and Utf8JsonWriter in System.Text.Json
This article shows how to use:
- A JSON Document Object Model (DOM) for random access to data in a JSON payload.
- The
Utf8JsonWriter
type for building custom serializers. - The
Utf8JsonReader
type for building custom parsers and deserializers.
JSON DOM choices
Working with a DOM is an alternative to deserialization with JsonSerializer:
- When you don't have a type to deserialize into.
- When the JSON you receive doesn't have a fixed schema and must be inspected to know what it contains.
System.Text.Json
provides two ways to build a JSON DOM:
- JsonDocument provides the ability to build a read-only DOM by using
Utf8JsonReader
. The JSON elements that compose the payload can be accessed via the JsonElement type. TheJsonElement
type provides array and object enumerators along with APIs to convert JSON text to common .NET types.JsonDocument
exposes a RootElement property. For more information, see Use JsonDocument later in this article.
- JsonNode and the classes that derive from it in the System.Text.Json.Nodes namespace provide the ability to create a mutable DOM. The JSON elements that compose the payload can be accessed via the JsonNode, JsonObject, JsonArray, JsonValue, and JsonElement types. For more information, see Use
JsonNode
later in this article.
Consider the following factors when choosing between JsonDocument
and JsonNode
:
- The
JsonNode
DOM can be changed after it's created. TheJsonDocument
DOM is immutable. - The
JsonDocument
DOM provides faster access to its data.
- Starting in .NET 6, JsonNode and the classes that derive from it in the System.Text.Json.Nodes namespace provide the ability to create a mutable DOM. For more information, see the .NET 6 version of this article.
Use JsonNode
The following example shows how to use JsonNode and the other types in the System.Text.Json.Nodes namespace to:
- Create a DOM from a JSON string
- Write JSON from a DOM.
- Get a value, object, or array from a DOM.
using System.Text.Json;
using System.Text.Json.Nodes;
namespace JsonNodeFromStringExample;
public class Program
{
public static void Main()
{
string jsonString =
@"{
""Date"": ""2019-08-01T00:00:00"",
""Temperature"": 25,
""Summary"": ""Hot"",
""DatesAvailable"": [
""2019-08-01T00:00:00"",
""2019-08-02T00:00:00""
],
""TemperatureRanges"": {
""Cold"": {
""High"": 20,
""Low"": -10
},
""Hot"": {
""High"": 60,
""Low"": 20
}
}
}
";
// Create a JsonNode DOM from a JSON string.
JsonNode forecastNode = JsonNode.Parse(jsonString)!;
// Write JSON from a JsonNode
var options = new JsonSerializerOptions { WriteIndented = true };
Console.WriteLine(forecastNode!.ToJsonString(options));
// output:
//{
// "Date": "2019-08-01T00:00:00",
// "Temperature": 25,
// "Summary": "Hot",
// "DatesAvailable": [
// "2019-08-01T00:00:00",
// "2019-08-02T00:00:00"
// ],
// "TemperatureRanges": {
// "Cold": {
// "High": 20,
// "Low": -10
// },
// "Hot": {
// "High": 60,
// "Low": 20
// }
// }
//}
// Get value from a JsonNode.
JsonNode temperatureNode = forecastNode!["Temperature"]!;
Console.WriteLine($"Type={temperatureNode.GetType()}");
Console.WriteLine($"JSON={temperatureNode.ToJsonString()}");
//output:
//Type = System.Text.Json.Nodes.JsonValue`1[System.Text.Json.JsonElement]
//JSON = 25
// Get a typed value from a JsonNode.
int temperatureInt = (int)forecastNode!["Temperature"]!;
Console.WriteLine($"Value={temperatureInt}");
//output:
//Value=25
// Get a typed value from a JsonNode by using GetValue<T>.
temperatureInt = forecastNode!["Temperature"]!.GetValue<int>();
Console.WriteLine($"TemperatureInt={temperatureInt}");
//output:
//Value=25
// Get a JSON object from a JsonNode.
JsonNode temperatureRanges = forecastNode!["TemperatureRanges"]!;
Console.WriteLine($"Type={temperatureRanges.GetType()}");
Console.WriteLine($"JSON={temperatureRanges.ToJsonString()}");
//output:
//Type = System.Text.Json.Nodes.JsonObject
//JSON = { "Cold":{ "High":20,"Low":-10},"Hot":{ "High":60,"Low":20} }
// Get a JSON array from a JsonNode.
JsonNode datesAvailable = forecastNode!["DatesAvailable"]!;
Console.WriteLine($"Type={datesAvailable.GetType()}");
Console.WriteLine($"JSON={datesAvailable.ToJsonString()}");
//output:
//datesAvailable Type = System.Text.Json.Nodes.JsonArray
//datesAvailable JSON =["2019-08-01T00:00:00", "2019-08-02T00:00:00"]
// Get an array element value from a JsonArray.
JsonNode firstDateAvailable = datesAvailable[0]!;
Console.WriteLine($"Type={firstDateAvailable.GetType()}");
Console.WriteLine($"JSON={firstDateAvailable.ToJsonString()}");
//output:
//Type = System.Text.Json.Nodes.JsonValue`1[System.Text.Json.JsonElement]
//JSON = "2019-08-01T00:00:00"
// Get a typed value by chaining references.
int coldHighTemperature = (int)forecastNode["TemperatureRanges"]!["Cold"]!["High"]!;
Console.WriteLine($"TemperatureRanges.Cold.High={coldHighTemperature}");
//output:
//TemperatureRanges.Cold.High = 20
// Parse a JSON array
var datesNode = JsonNode.Parse(@"[""2019-08-01T00:00:00"",""2019-08-02T00:00:00""]");
JsonNode firstDate = datesNode![0]!.GetValue<DateTime>();
Console.WriteLine($"firstDate={ firstDate}");
//output:
//firstDate = "2019-08-01T00:00:00"
}
}
Create a JsonNode DOM with object initializers and make changes
The following example shows how to:
- Create a DOM by using object initializers.
- Make changes to a DOM.
using System.Text.Json;
using System.Text.Json.Nodes;
namespace JsonNodeFromObjectExample;
public class Program
{
public static void Main()
{
// Create a new JsonObject using object initializers.
var forecastObject = new JsonObject
{
["Date"] = new DateTime(2019, 8, 1),
["Temperature"] = 25,
["Summary"] = "Hot",
["DatesAvailable"] = new JsonArray(
new DateTime(2019, 8, 1), new DateTime(2019, 8, 2)),
["TemperatureRanges"] = new JsonObject
{
["Cold"] = new JsonObject
{
["High"] = 20,
["Low"] = -10
}
},
["SummaryWords"] = new JsonArray("Cool", "Windy", "Humid")
};
// Add an object.
forecastObject!["TemperatureRanges"]!["Hot"] =
new JsonObject { ["High"] = 60, ["Low"] = 20 };
// Remove a property.
forecastObject.Remove("SummaryWords");
// Change the value of a property.
forecastObject["Date"] = new DateTime(2019, 8, 3);
var options = new JsonSerializerOptions { WriteIndented = true };
Console.WriteLine(forecastObject.ToJsonString(options));
//output:
//{
// "Date": "2019-08-03T00:00:00",
// "Temperature": 25,
// "Summary": "Hot",
// "DatesAvailable": [
// "2019-08-01T00:00:00",
// "2019-08-02T00:00:00"
// ],
// "TemperatureRanges": {
// "Cold": {
// "High": 20,
// "Low": -10
// },
// "Hot": {
// "High": 60,
// "Low": 20
// }
// }
//}
}
}
Deserialize subsections of a JSON payload
The following example shows how to use JsonNode to navigate to a subsection of a JSON tree and deserialize a single value, a custom type, or an array from that subsection.
using System.Text.Json;
using System.Text.Json.Nodes;
namespace JsonNodePOCOExample;
public class TemperatureRanges : Dictionary<string, HighLowTemps>
{
}
public class HighLowTemps
{
public int High { get; set; }
public int Low { get; set; }
}
public class Program
{
public static DateTime[]? DatesAvailable { get; set; }
public static void Main()
{
string jsonString =
@"{
""Date"": ""2019-08-01T00:00:00"",
""Temperature"": 25,
""Summary"": ""Hot"",
""DatesAvailable"": [
""2019-08-01T00:00:00"",
""2019-08-02T00:00:00""
],
""TemperatureRanges"": {
""Cold"": {
""High"": 20,
""Low"": -10
},
""Hot"": {
""High"": 60,
""Low"": 20
}
}
}
";
// Parse all of the JSON.
JsonNode forecastNode = JsonNode.Parse(jsonString)!;
// Get a single value
int hotHigh = forecastNode["TemperatureRanges"]!["Hot"]!["High"]!.GetValue<int>();
Console.WriteLine($"Hot.High={hotHigh}");
// output:
//Hot.High=60
// Get a subsection and deserialize it into a custom type.
JsonObject temperatureRangesObject = forecastNode!["TemperatureRanges"]!.AsObject();
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream);
temperatureRangesObject.WriteTo(writer);
writer.Flush();
TemperatureRanges? temperatureRanges =
JsonSerializer.Deserialize<TemperatureRanges>(stream.ToArray());
Console.WriteLine($"Cold.Low={temperatureRanges!["Cold"].Low}, Hot.High={temperatureRanges["Hot"].High}");
// output:
//Cold.Low=-10, Hot.High=60
// Get a subsection and deserialize it into an array.
JsonArray datesAvailable = forecastNode!["DatesAvailable"]!.AsArray()!;
Console.WriteLine($"DatesAvailable[0]={datesAvailable[0]}");
// output:
//DatesAvailable[0]=8/1/2019 12:00:00 AM
}
}
JsonNode average grade example
The following example selects a JSON array that has integer values and calculates an average value:
using System.Text.Json.Nodes;
namespace JsonNodeAverageGradeExample;
public class Program
{
public static void Main()
{
string jsonString =
@"{
""Class Name"": ""Science"",
""Teacher\u0027s Name"": ""Jane"",
""Semester"": ""2019-01-01"",
""Students"": [
{
""Name"": ""John"",
""Grade"": 94.3
},
{
""Name"": ""James"",
""Grade"": 81.0
},
{
""Name"": ""Julia"",
""Grade"": 91.9
},
{
""Name"": ""Jessica"",
""Grade"": 72.4
},
{
""Name"": ""Johnathan""
}
],
""Final"": true
}
";
double sum = 0;
int count = 0;
JsonNode document = JsonNode.Parse(jsonString)!;
JsonNode root = document.Root;
JsonArray studentsArray = root["Students"]!.AsArray();
count = studentsArray.Count;
foreach (JsonNode? student in studentsArray)
{
if (student?["Grade"] is JsonNode gradeNode)
{
sum += (double)gradeNode;
}
else
{
sum += 70;
}
}
double average = sum / count;
Console.WriteLine($"Average grade : {average}");
}
}
// output:
//Average grade : 81.92
The preceding code:
- Calculates an average grade for objects in a
Students
array that have aGrade
property. - Assigns a default grade of 70 for students who don't have a grade.
- Gets the number of students from the
Count
property ofJsonArray
.
JsonNode
with JsonSerializerOptions
You can use JsonSerializer
to serialize and deserialize an instance of JsonNode
. However, if you use an overload that takes JsonSerializerOptions
, the options instance is only used to get custom converters. Other features of the options instance are not used. For example, if you set JsonSerializerOptions.DefaultIgnoreCondition to WhenWritingNull and call JsonSerializer
with an overload that takes JsonSerializerOptions
, null properties won't be ignored.
The same limitation applies to the JsonNode
methods that take a JsonSerializerOptions
parameter: WriteTo(Utf8JsonWriter, JsonSerializerOptions) and ToJsonString(JsonSerializerOptions). These APIs use JsonSerializerOptions
only to get custom converters.
The following example illustrates the result of using methods that take a JsonSerializerOptions
parameter and serialize a JsonNode
instance:
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace JsonNodeWithJsonSerializerOptions;
public class Program
{
public static void Main()
{
Person person = new Person { Name = "Nancy" };
// Default serialization - Address property included with null token.
// Output: {"Name":"Nancy","Address":null}
string personJsonWithNull = JsonSerializer.Serialize(person);
Console.WriteLine(personJsonWithNull);
// Serialize and ignore null properties - null Address property is omitted
// Output: {"Name":"Nancy"}
JsonSerializerOptions options = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
string personJsonWithoutNull = JsonSerializer.Serialize(person, options);
Console.WriteLine(personJsonWithoutNull);
// Ignore null properties doesn't work when serializing JsonNode instance
// by using JsonSerializer.
// Output: {"Name":"Nancy","Address":null}
var personJsonNode = JsonSerializer.Deserialize<JsonNode>(personJsonWithNull);
personJsonWithNull = JsonSerializer.Serialize(personJsonNode, options);
Console.WriteLine(personJsonWithNull);
// Ignore null properties doesn't work when serializing JsonNode instance
// by using JsonNode.ToJsonString method.
// Output: {"Name":"Nancy","Address":null}
personJsonWithNull = personJsonNode!.ToJsonString(options);
Console.WriteLine(personJsonWithNull);
// Ignore null properties doesn't work when serializing JsonNode instance
// by using JsonNode.WriteTo method.
// Output: {"Name":"Nancy","Address":null}
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream);
personJsonNode!.WriteTo(writer, options);
writer.Flush();
personJsonWithNull = Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(personJsonWithNull);
}
}
public class Person
{
public string? Name { get; set; }
public string? Address { get; set; }
}
If you need features of JsonSerializerOptions
other than custom converters, use JsonSerializer
with strongly typed targets (such as the Person
class in this example) rather than JsonNode
.
Use JsonDocument
The following example shows how to use the JsonDocument class for random access to data in a JSON string:
double sum = 0;
int count = 0;
using (JsonDocument document = JsonDocument.Parse(jsonString))
{
JsonElement root = document.RootElement;
JsonElement studentsElement = root.GetProperty("Students");
foreach (JsonElement student in studentsElement.EnumerateArray())
{
if (student.TryGetProperty("Grade", out JsonElement gradeElement))
{
sum += gradeElement.GetDouble();
}
else
{
sum += 70;
}
count++;
}
}
double average = sum / count;
Console.WriteLine($"Average grade : {average}");
Dim sum As Double = 0
Dim count As Integer = 0
Using document As JsonDocument = JsonDocument.Parse(jsonString)
Dim root As JsonElement = document.RootElement
Dim studentsElement As JsonElement = root.GetProperty("Students")
For Each student As JsonElement In studentsElement.EnumerateArray()
Dim gradeElement As JsonElement = Nothing
If student.TryGetProperty("Grade", gradeElement) Then
sum += gradeElement.GetDouble()
Else
sum += 70
End If
count += 1
Next
End Using
Dim average As Double = sum / count
Console.WriteLine($"Average grade : {average}")
The preceding code:
- Assumes the JSON to analyze is in a string named
jsonString
. - Calculates an average grade for objects in a
Students
array that have aGrade
property. - Assigns a default grade of 70 for students who don't have a grade.
- Creates the
JsonDocument
instance in ausing
statement becauseJsonDocument
implementsIDisposable
. After aJsonDocument
instance is disposed, you lose access to all of itsJsonElement
instances also. To retain access to aJsonElement
instance, make a copy of it before the parentJsonDocument
instance is disposed. To make a copy, call JsonElement.Clone. For more information, see JsonDocument is IDisposable.
The preceding example code counts students by incrementing a count
variable with each iteration. An alternative is to call GetArrayLength, as shown in the following example:
double sum = 0;
int count = 0;
using (JsonDocument document = JsonDocument.Parse(jsonString))
{
JsonElement root = document.RootElement;
JsonElement studentsElement = root.GetProperty("Students");
count = studentsElement.GetArrayLength();
foreach (JsonElement student in studentsElement.EnumerateArray())
{
if (student.TryGetProperty("Grade", out JsonElement gradeElement))
{
sum += gradeElement.GetDouble();
}
else
{
sum += 70;
}
}
}
double average = sum / count;
Console.WriteLine($"Average grade : {average}");
Dim sum As Double = 0
Dim count As Integer = 0
Using document As JsonDocument = JsonDocument.Parse(jsonString)
Dim root As JsonElement = document.RootElement
Dim studentsElement As JsonElement = root.GetProperty("Students")
count = studentsElement.GetArrayLength()
For Each student As JsonElement In studentsElement.EnumerateArray()
Dim gradeElement As JsonElement = Nothing
If student.TryGetProperty("Grade", gradeElement) Then
sum += gradeElement.GetDouble()
Else
sum += 70
End If
Next
End Using
Dim average As Double = sum / count
Console.WriteLine($"Average grade : {average}")
Here's an example of the JSON that this code processes:
{
"Class Name": "Science",
"Teacher\u0027s Name": "Jane",
"Semester": "2019-01-01",
"Students": [
{
"Name": "John",
"Grade": 94.3
},
{
"Name": "James",
"Grade": 81.0
},
{
"Name": "Julia",
"Grade": 91.9
},
{
"Name": "Jessica",
"Grade": 72.4
},
{
"Name": "Johnathan"
}
],
"Final": true
}
For a similar example that uses JsonNode
instead of JsonDocument
, see JsonNode average grade example.
How to search a JsonDocument and JsonElement for sub-elements
Searches on JsonElement
require a sequential search of the properties and hence are relatively slow (for example when using TryGetProperty
). System.Text.Json is designed to minimize initial parse time rather than lookup time. Therefore, use the following approaches to optimize performance when searching through a JsonDocument
object:
- Use the built-in enumerators (EnumerateArray and EnumerateObject) rather than doing your own indexing or loops.
- Don't do a sequential search on the whole
JsonDocument
through every property by usingRootElement
. Instead, search on nested JSON objects based on the known structure of the JSON data. For example, the preceding code examples look for aGrade
property inStudent
objects by looping through theStudent
objects and getting the value ofGrade
for each, rather than searching through allJsonElement
objects looking forGrade
properties. Doing the latter would result in unnecessary passes over the same data.
Use JsonDocument
to write JSON
The following example shows how to write JSON from a JsonDocument:
string jsonString = File.ReadAllText(inputFileName);
var writerOptions = new JsonWriterOptions
{
Indented = true
};
var documentOptions = new JsonDocumentOptions
{
CommentHandling = JsonCommentHandling.Skip
};
using FileStream fs = File.Create(outputFileName);
using var writer = new Utf8JsonWriter(fs, options: writerOptions);
using JsonDocument document = JsonDocument.Parse(jsonString, documentOptions);
JsonElement root = document.RootElement;
if (root.ValueKind == JsonValueKind.Object)
{
writer.WriteStartObject();
}
else
{
return;
}
foreach (JsonProperty property in root.EnumerateObject())
{
property.WriteTo(writer);
}
writer.WriteEndObject();
writer.Flush();
Dim jsonString As String = File.ReadAllText(inputFileName)
Dim writerOptions As JsonWriterOptions = New JsonWriterOptions With {
.Indented = True
}
Dim documentOptions As JsonDocumentOptions = New JsonDocumentOptions With {
.CommentHandling = JsonCommentHandling.Skip
}
Dim fs As FileStream = File.Create(outputFileName)
Dim writer As Utf8JsonWriter = New Utf8JsonWriter(fs, options:=writerOptions)
Dim document As JsonDocument = JsonDocument.Parse(jsonString, documentOptions)
Dim root As JsonElement = document.RootElement
If root.ValueKind = JsonValueKind.[Object] Then
writer.WriteStartObject()
Else
Return
End If
For Each [property] As JsonProperty In root.EnumerateObject()
[property].WriteTo(writer)
Next
writer.WriteEndObject()
writer.Flush()
The preceding code:
- Reads a JSON file, loads the data into a
JsonDocument
, and writes formatted (pretty-printed) JSON to a file. - Uses JsonDocumentOptions to specify that comments in the input JSON are allowed but ignored.
- When finished, calls Flush on the writer. An alternative is to let the writer auto-flush when it's disposed.
Here's an example of JSON input to be processed by the example code:
{"Class Name": "Science","Teacher's Name": "Jane","Semester": "2019-01-01","Students": [{"Name": "John","Grade": 94.3},{"Name": "James","Grade": 81.0},{"Name": "Julia","Grade": 91.9},{"Name": "Jessica","Grade": 72.4},{"Name": "Johnathan"}],"Final": true}
The result is the following pretty-printed JSON output:
{
"Class Name": "Science",
"Teacher\u0027s Name": "Jane",
"Semester": "2019-01-01",
"Students": [
{
"Name": "John",
"Grade": 94.3
},
{
"Name": "James",
"Grade": 81.0
},
{
"Name": "Julia",
"Grade": 91.9
},
{
"Name": "Jessica",
"Grade": 72.4
},
{
"Name": "Johnathan"
}
],
"Final": true
}
JsonDocument is IDisposable
JsonDocument
builds an in-memory view of the data into a pooled buffer. Therefore the JsonDocument
type implements IDisposable
and needs to be used inside a using
block.
Only return a JsonDocument
from your API if you want to transfer lifetime ownership and dispose responsibility to the caller. In most scenarios, that isn't necessary. If the caller needs to work with the entire JSON document, return the Clone of the RootElement, which is a JsonElement. If the caller needs to work with a particular element within the JSON document, return the Clone of that JsonElement. If you return the RootElement
or a sub-element directly without making a Clone
, the caller won't be able to access the returned JsonElement
after the JsonDocument
that owns it is disposed.
Here's an example that requires you to make a Clone
:
public JsonElement LookAndLoad(JsonElement source)
{
string json = File.ReadAllText(source.GetProperty("fileName").GetString());
using (JsonDocument doc = JsonDocument.Parse(json))
{
return doc.RootElement.Clone();
}
}
The preceding code expects a JsonElement
that contains a fileName
property. It opens the JSON file and creates a JsonDocument
. The method assumes that the caller wants to work with the entire document, so it returns the Clone
of the RootElement
.
If you receive a JsonElement
and are returning a sub-element, it's not necessary to return a Clone
of the sub-element. The caller is responsible for keeping alive the JsonDocument
that the passed-in JsonElement
belongs to. For example:
public JsonElement ReturnFileName(JsonElement source)
{
return source.GetProperty("fileName");
}
JsonDocument
with JsonSerializerOptions
You can use JsonSerializer
to serialize and deserialize an instance of JsonDocument
. However, the implementation for reading and writing JsonDocument
instances by using JsonSerializer
is a wrapper over the JsonDocument.ParseValue(Utf8JsonReader) and JsonDocument.WriteTo(Utf8JsonWriter). This wrapper does not forward any JsonSerializerOptions
(serializer features) to Utf8JsonReader
or Utf8JsonWriter
. For example, if you set JsonSerializerOptions.DefaultIgnoreCondition to WhenWritingNull and call JsonSerializer
with an overload that takes JsonSerializerOptions
, null properties won't be ignored.
The following example illustrates the result of using methods that take a JsonSerializerOptions
parameter and serialize a JsonDocument
instance:
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace JsonDocumentWithJsonSerializerOptions;
public class Program
{
public static void Main()
{
Person person = new Person { Name = "Nancy" };
// Default serialization - Address property included with null token.
// Output: {"Name":"Nancy","Address":null}
string personJsonWithNull = JsonSerializer.Serialize(person);
Console.WriteLine(personJsonWithNull);
// Serialize and ignore null properties - null Address property is omitted
// Output: {"Name":"Nancy"}
JsonSerializerOptions options = new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
string personJsonWithoutNull = JsonSerializer.Serialize(person, options);
Console.WriteLine(personJsonWithoutNull);
// Ignore null properties doesn't work when serializing JsonDocument instance
// by using JsonSerializer.
// Output: {"Name":"Nancy","Address":null}
var personJsonDocument = JsonSerializer.Deserialize<JsonDocument>(personJsonWithNull);
personJsonWithNull = JsonSerializer.Serialize(personJsonDocument, options);
Console.WriteLine(personJsonWithNull);
}
}
public class Person
{
public string? Name { get; set; }
public string? Address { get; set; }
}
If you need features of JsonSerializerOptions
, use JsonSerializer
with strongly typed targets (such as the Person
class in this example) rather than JsonDocument
.
Use Utf8JsonWriter
Utf8JsonWriter is a high-performance way to write UTF-8 encoded JSON text from common .NET types like String
, Int32
, and DateTime
. The writer is a low-level type that can be used to build custom serializers. The JsonSerializer.Serialize method uses Utf8JsonWriter
under the covers.
The following example shows how to use the Utf8JsonWriter class:
var options = new JsonWriterOptions
{
Indented = true
};
using var stream = new MemoryStream();
using var writer = new Utf8JsonWriter(stream, options);
writer.WriteStartObject();
writer.WriteString("date", DateTimeOffset.UtcNow);
writer.WriteNumber("temp", 42);
writer.WriteEndObject();
writer.Flush();
string json = Encoding.UTF8.GetString(stream.ToArray());
Console.WriteLine(json);
Dim options As JsonWriterOptions = New JsonWriterOptions With {
.Indented = True
}
Dim stream As MemoryStream = New MemoryStream
Dim writer As Utf8JsonWriter = New Utf8JsonWriter(stream, options)
writer.WriteStartObject()
writer.WriteString("date", DateTimeOffset.UtcNow)
writer.WriteNumber("temp", 42)
writer.WriteEndObject()
writer.Flush()
Dim json As String = Encoding.UTF8.GetString(stream.ToArray())
Console.WriteLine(json)
Write with UTF-8 text
To achieve the best possible performance while using the Utf8JsonWriter
, write JSON payloads already encoded as UTF-8 text rather than as UTF-16 strings. Use JsonEncodedText to cache and pre-encode known string property names and values as statics, and pass those to the writer, rather than using UTF-16 string literals. This is faster than caching and using UTF-8 byte arrays.
This approach also works if you need to do custom escaping. System.Text.Json
doesn't let you disable escaping while writing a string. However, you could pass in your own custom JavaScriptEncoder as an option to the writer, or create your own JsonEncodedText
that uses your JavascriptEncoder
to do the escaping, and then write the JsonEncodedText
instead of the string. For more information, see Customize character encoding.
Write raw JSON
In some scenarios, you might want to write "raw" JSON to a JSON payload that you're creating with Utf8JsonWriter
. You can use Utf8JsonWriter.WriteRawValue to do that. Here are typical scenarios:
You have an existing JSON payload that you want to enclose in new JSON.
You want to format values differently from the default
Utf8JsonWriter
formatting.For example, you might want to customize number formatting. By default, System.Text.Json omits the decimal point for whole numbers, writing
1
rather than1.0
, for example. The rationale is that writing fewer bytes is good for performance. But suppose the consumer of your JSON treats numbers with decimals as doubles, and numbers without decimals as integers. You might want to ensure that the numbers in an array are all recognized as doubles, by writing a decimal point and zero for whole numbers. The following example shows how to do that:using System.Text; using System.Text.Json; namespace WriteRawJson; public class Program { public static void Main() { JsonWriterOptions writerOptions = new() { Indented = true, }; using MemoryStream stream = new(); using Utf8JsonWriter writer = new(stream, writerOptions); writer.WriteStartObject(); writer.WriteStartArray("defaultJsonFormatting"); foreach (double number in new double[] { 50.4, 51 }) { writer.WriteStartObject(); writer.WritePropertyName("value"); writer.WriteNumberValue(number); writer.WriteEndObject(); } writer.WriteEndArray(); writer.WriteStartArray("customJsonFormatting"); foreach (double result in new double[] { 50.4, 51 }) { writer.WriteStartObject(); writer.WritePropertyName("value"); writer.WriteRawValue( FormatNumberValue(result), skipInputValidation: true); writer.WriteEndObject(); } writer.WriteEndArray(); writer.WriteEndObject(); writer.Flush(); string json = Encoding.UTF8.GetString(stream.ToArray()); Console.WriteLine(json); } static string FormatNumberValue(double numberValue) { return numberValue == Convert.ToInt32(numberValue) ? numberValue.ToString() + ".0" : numberValue.ToString(); } } // output: //{ // "defaultJsonFormatting": [ // { // "value": 50.4 // }, // { // "value": 51 // } // ], // "customJsonFormatting": [ // { // "value": 50.4 // }, // { // "value": 51.0 // } // ] //}
Customize character escaping
The StringEscapeHandling setting of JsonTextWriter
offers options to escape all non-ASCII characters or HTML characters. By default, Utf8JsonWriter
escapes all non-ASCII and HTML characters. This escaping is done for defense-in-depth security reasons. To specify a different escaping policy, create a JavaScriptEncoder and set JsonWriterOptions.Encoder. For more information, see Customize character encoding.
Write null values
To write null values by using Utf8JsonWriter
, call:
- WriteNull to write a key-value pair with null as the value.
- WriteNullValue to write null as an element of a JSON array.
For a string property, if the string is null, WriteString and WriteStringValue are equivalent to WriteNull
and WriteNullValue
.
Write Timespan, Uri, or char values
To write Timespan
, Uri
, or char
values, format them as strings (by calling ToString()
, for example) and call WriteStringValue.
Use Utf8JsonReader
Utf8JsonReader is a high-performance, low allocation, forward-only reader for UTF-8 encoded JSON text, read from a ReadOnlySpan<byte>
or ReadOnlySequence<byte>
. The Utf8JsonReader
is a low-level type that can be used to build custom parsers and deserializers. The JsonSerializer.Deserialize method uses Utf8JsonReader
under the covers.
Utf8JsonReader
can't be used directly from Visual Basic code. For more information, see Visual Basic support.
The following example shows how to use the Utf8JsonReader class:
var options = new JsonReaderOptions
{
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip
};
var reader = new Utf8JsonReader(jsonUtf8Bytes, options);
while (reader.Read())
{
Console.Write(reader.TokenType);
switch (reader.TokenType)
{
case JsonTokenType.PropertyName:
case JsonTokenType.String:
{
string? text = reader.GetString();
Console.Write(" ");
Console.Write(text);
break;
}
case JsonTokenType.Number:
{
int intValue = reader.GetInt32();
Console.Write(" ");
Console.Write(intValue);
break;
}
// Other token types elided for brevity
}
Console.WriteLine();
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://docs.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support
The preceding code assumes that the jsonUtf8
variable is a byte array that contains valid JSON, encoded as UTF-8.
Filter data using Utf8JsonReader
The following example shows how to synchronously read a file and search for a value.
using System.Text;
using System.Text.Json;
namespace SystemTextJsonSamples
{
public class Utf8ReaderFromFile
{
private static readonly byte[] s_nameUtf8 = Encoding.UTF8.GetBytes("name");
private static ReadOnlySpan<byte> Utf8Bom => new byte[] { 0xEF, 0xBB, 0xBF };
public static void Run()
{
// ReadAllBytes if the file encoding is UTF-8:
string fileName = "UniversitiesUtf8.json";
ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);
// Read past the UTF-8 BOM bytes if a BOM exists.
if (jsonReadOnlySpan.StartsWith(Utf8Bom))
{
jsonReadOnlySpan = jsonReadOnlySpan.Slice(Utf8Bom.Length);
}
// Or read as UTF-16 and transcode to UTF-8 to convert to a ReadOnlySpan<byte>
//string fileName = "Universities.json";
//string jsonString = File.ReadAllText(fileName);
//ReadOnlySpan<byte> jsonReadOnlySpan = Encoding.UTF8.GetBytes(jsonString);
int count = 0;
int total = 0;
var reader = new Utf8JsonReader(jsonReadOnlySpan);
while (reader.Read())
{
JsonTokenType tokenType = reader.TokenType;
switch (tokenType)
{
case JsonTokenType.StartObject:
total++;
break;
case JsonTokenType.PropertyName:
if (reader.ValueTextEquals(s_nameUtf8))
{
// Assume valid JSON, known schema
reader.Read();
if (reader.GetString()!.EndsWith("University"))
{
count++;
}
}
break;
}
}
Console.WriteLine($"{count} out of {total} have names that end with 'University'");
}
}
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://docs.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support
For an asynchronous version of this example, see .NET samples JSON project.
The preceding code:
Assumes the JSON contains an array of objects and each object may contain a "name" property of type string.
Counts objects and "name" property values that end with "University".
Assumes the file is encoded as UTF-16 and transcodes it into UTF-8. A file encoded as UTF-8 can be read directly into a
ReadOnlySpan<byte>
by using the following code:ReadOnlySpan<byte> jsonReadOnlySpan = File.ReadAllBytes(fileName);
If the file contains a UTF-8 byte order mark (BOM), remove it before passing the bytes to the
Utf8JsonReader
, since the reader expects text. Otherwise, the BOM is considered invalid JSON, and the reader throws an exception.
Here's a JSON sample that the preceding code can read. The resulting summary message is "2 out of 4 have names that end with 'University'":
[
{
"web_pages": [ "https://contoso.edu/" ],
"alpha_two_code": "US",
"state-province": null,
"country": "United States",
"domains": [ "contoso.edu" ],
"name": "Contoso Community College"
},
{
"web_pages": [ "http://fabrikam.edu/" ],
"alpha_two_code": "US",
"state-province": null,
"country": "United States",
"domains": [ "fabrikam.edu" ],
"name": "Fabrikam Community College"
},
{
"web_pages": [ "http://www.contosouniversity.edu/" ],
"alpha_two_code": "US",
"state-province": null,
"country": "United States",
"domains": [ "contosouniversity.edu" ],
"name": "Contoso University"
},
{
"web_pages": [ "http://www.fabrikamuniversity.edu/" ],
"alpha_two_code": "US",
"state-province": null,
"country": "United States",
"domains": [ "fabrikamuniversity.edu" ],
"name": "Fabrikam University"
}
]
Consume decoded JSON strings
Starting in .NET 7, you can use the Utf8JsonReader.CopyString method instead of Utf8JsonReader.GetString() to consume a decoded JSON string. Unlike GetString(), which always allocates a new string, CopyString lets you copy the unescaped string to a buffer that you own. The following code snippet shows an example of consuming a UTF-16 string using CopyString.
int valueLength = reader.HasReadOnlySequence
? checked((int)ValueSequence.Length)
: ValueSpan.Length;
char[] buffer = ArrayPool<char>.Shared.Rent(valueLength);
int charsRead = reader.CopyString(buffer);
ReadOnlySpan<char> source = buffer.Slice(0, charsRead);
// Handle the unescaped JSON string.
ParseUnescapedString(source);
ArrayPool<char>.Shared.Return(buffer, clearArray: true);
Read from a stream using Utf8JsonReader
When reading a large file (a gigabyte or more in size, for example), you might want to avoid having to load the entire file into memory at once. For this scenario, you can use a FileStream.
When using the Utf8JsonReader
to read from a stream, the following rules apply:
- The buffer containing the partial JSON payload must be at least as large as the largest JSON token within it so that the reader can make forward progress.
- The buffer must be at least as large as the largest sequence of white space within the JSON.
- The reader doesn't keep track of the data it has read until it completely reads the next TokenType in the JSON payload. So when there are bytes left over in the buffer, you have to pass them to the reader again. You can use BytesConsumed to determine how many bytes are left over.
The following code illustrates how to read from a stream. The example shows a MemoryStream. Similar code will work with a FileStream, except when the FileStream
contains a UTF-8 BOM at the start. In that case, you need to strip those three bytes from the buffer before passing the remaining bytes to the Utf8JsonReader
. Otherwise the reader would throw an exception, since the BOM is not considered a valid part of the JSON.
The sample code starts with a 4KB buffer and doubles the buffer size each time it finds that the size is not large enough to fit a complete JSON token, which is required for the reader to make forward progress on the JSON payload. The JSON sample provided in the snippet triggers a buffer size increase only if you set a very small initial buffer size, for example, 10 bytes. If you set the initial buffer size to 10, the Console.WriteLine
statements illustrate the cause and effect of buffer size increases. At the 4KB initial buffer size, the entire sample JSON is shown by each Console.WriteLine
, and the buffer size never has to be increased.
using System.Text;
using System.Text.Json;
namespace SystemTextJsonSamples
{
public class Utf8ReaderPartialRead
{
public static void Run()
{
var jsonString = @"{
""Date"": ""2019-08-01T00:00:00-07:00"",
""Temperature"": 25,
""TemperatureRanges"": {
""Cold"": { ""High"": 20, ""Low"": -10 },
""Hot"": { ""High"": 60, ""Low"": 20 }
},
""Summary"": ""Hot"",
}";
byte[] bytes = Encoding.UTF8.GetBytes(jsonString);
var stream = new MemoryStream(bytes);
var buffer = new byte[4096];
// Fill the buffer.
// For this snippet, we're assuming the stream is open and has data.
// If it might be closed or empty, check if the return value is 0.
stream.Read(buffer);
// We set isFinalBlock to false since we expect more data in a subsequent read from the stream.
var reader = new Utf8JsonReader(buffer, isFinalBlock: false, state: default);
Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
// Search for "Summary" property name
while (reader.TokenType != JsonTokenType.PropertyName || !reader.ValueTextEquals("Summary"))
{
if (!reader.Read())
{
// Not enough of the JSON is in the buffer to complete a read.
GetMoreBytesFromStream(stream, buffer, ref reader);
}
}
// Found the "Summary" property name.
Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
while (!reader.Read())
{
// Not enough of the JSON is in the buffer to complete a read.
GetMoreBytesFromStream(stream, buffer, ref reader);
}
// Display value of Summary property, that is, "Hot".
Console.WriteLine($"Got property value: {reader.GetString()}");
}
private static void GetMoreBytesFromStream(
MemoryStream stream, byte[] buffer, ref Utf8JsonReader reader)
{
int bytesRead;
if (reader.BytesConsumed < buffer.Length)
{
ReadOnlySpan<byte> leftover = buffer.AsSpan((int)reader.BytesConsumed);
if (leftover.Length == buffer.Length)
{
Array.Resize(ref buffer, buffer.Length * 2);
Console.WriteLine($"Increased buffer size to {buffer.Length}");
}
leftover.CopyTo(buffer);
bytesRead = stream.Read(buffer.AsSpan(leftover.Length));
}
else
{
bytesRead = stream.Read(buffer);
}
Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}");
reader = new Utf8JsonReader(buffer, isFinalBlock: bytesRead == 0, reader.CurrentState);
}
}
}
' This code example doesn't apply to Visual Basic. For more information, go to the following URL:
' https://docs.microsoft.com/dotnet/standard/serialization/system-text-json-how-to#visual-basic-support
The preceding example sets no limit to how large the buffer can grow. If the token size is too large, the code could fail with an OutOfMemoryException exception. This can happen if the JSON contains a token that is around 1 GB or more in size, because doubling the 1 GB size results in a size that is too large to fit into an int32
buffer.
Utf8JsonReader is a ref struct
Because the Utf8JsonReader
type is a ref struct, it has certain limitations. For example, it can't be stored as a field on a class or struct other than a ref struct. To achieve high performance, this type must be a ref struct
since it needs to cache the input ReadOnlySpan<byte>, which itself is a ref struct. In addition, this type is mutable since it holds state. Therefore, pass it by reference rather than by value. Passing it by value would result in a struct copy and the state changes would not be visible to the caller. For more information about how to use ref structs, see Avoid allocations.
Read UTF-8 text
To achieve the best possible performance while using the Utf8JsonReader
, read JSON payloads already encoded as UTF-8 text rather than as UTF-16 strings. For a code example, see Filter data using Utf8JsonReader.
Read with multi-segment ReadOnlySequence
If your JSON input is a ReadOnlySpan<byte>, each JSON element can be accessed from the ValueSpan
property on the reader as you go through the read loop. However, if your input is a ReadOnlySequence<byte> (which is the result of reading from a PipeReader), some JSON elements might straddle multiple segments of the ReadOnlySequence<byte>
object. These elements would not be accessible from ValueSpan in a contiguous memory block. Instead, whenever you have a multi-segment ReadOnlySequence<byte>
as input, poll the HasValueSequence property on the reader to figure out how to access the current JSON element. Here's a recommended pattern:
while (reader.Read())
{
switch (reader.TokenType)
{
// ...
ReadOnlySpan<byte> jsonElement = reader.HasValueSequence ?
reader.ValueSequence.ToArray() :
reader.ValueSpan;
// ...
}
}
Use ValueTextEquals for property name lookups
Don't use ValueSpan to do byte-by-byte comparisons by calling SequenceEqual for property name lookups. Call ValueTextEquals instead, because that method unescapes any characters that are escaped in the JSON. Here's an example that shows how to search for a property that is named "name":
private static readonly byte[] s_nameUtf8 = Encoding.UTF8.GetBytes("name");
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
total++;
break;
case JsonTokenType.PropertyName:
if (reader.ValueTextEquals(s_nameUtf8))
{
count++;
}
break;
}
}
Read null values into nullable value types
The built-in System.Text.Json
APIs return only non-nullable value types. For example, Utf8JsonReader.GetBoolean returns a bool
. It throws an exception if it finds Null
in the JSON. The following examples show two ways to handle nulls, one by returning a nullable value type and one by returning the default value:
public bool? ReadAsNullableBoolean()
{
_reader.Read();
if (_reader.TokenType == JsonTokenType.Null)
{
return null;
}
if (_reader.TokenType != JsonTokenType.True && _reader.TokenType != JsonTokenType.False)
{
throw new JsonException();
}
return _reader.GetBoolean();
}
public bool ReadAsBoolean(bool defaultValue)
{
_reader.Read();
if (_reader.TokenType == JsonTokenType.Null)
{
return defaultValue;
}
if (_reader.TokenType != JsonTokenType.True && _reader.TokenType != JsonTokenType.False)
{
throw new JsonException();
}
return _reader.GetBoolean();
}
See also
- System.Text.Json overview
- Instantiate JsonSerializerOptions instances
- Enable case-insensitive matching
- Customize property names and values
- Ignore properties
- Allow invalid JSON
- Handle overflow JSON or use JsonElement or JsonNode
- Preserve references and handle circular references
- Deserialize to immutable types and non-public accessors
- Polymorphic serialization
- Migrate from Newtonsoft.Json to System.Text.Json
- Customize character encoding
- Use DOM, Utf8JsonReader, and Utf8JsonWriter
- Write custom converters for JSON serialization
- DateTime and DateTimeOffset support
- How to use source generation
- Supported collection types
- System.Text.Json API reference
- System.Text.Json.Serialization API reference
Feedback
Submit and view feedback for