What's new in .NET 9: System.Text.Json improvements
.NET 9 is releasing in mid-November 2024. Like every .NET version, this introduces several important features and enhancements aligning developers with an ever-changing development ecosystem. In this blog series, I will explore critical updates in different areas of .NET. For this post, I will look through advancements in System.Text.Json
.
What is System.Text.Json in .NET?
System.Text.json
is a .NET package that delivers a powerful, high-performance solution for handling JSON in .NET applications. It offers fast, efficient, and low-memory serialization and deserialization of .NET objects to and from JSON. The package supports universal UTF-8 encoding and can work with any application and browser.
Equipped with robust types for reading and writing JSON, it offers both a high-performance, read-only JSON document model for random access and a mutable DOM that works seamlessly with the JSON serializer. Developers benefit from advanced features, including async serialization with IAsyncEnumerable
support and a highly customizable contract model, making it an essential tool for any .NET application working with JSON data.
.NET 9 brings a few novelties in Sytem.Text.Json
. To install the package, run the following command:
dotnet add package System.Text.Json
JSON Schema Exporter
The new JsonSchemaExporter
class allows you to extract schema information for any .NET type. JsonSerializerOptions
or JsonTypeInfo
instances help you with that.
Code example
Let's create new files to hold all the types:
Inside Student.cs
define the record type:
record Student(string Name, int Age, string? Department = null);
In Program.cs
:
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Schema;
JsonSerializerOptions options = JsonSerializerOptions.Default;
JsonNode schema = options.GetJsonSchemaAsNode(typeof(Student));
Console.WriteLine(schema.ToString());
Output:
{
"type": [
"object",
"null"
],
"properties": {
"Name": {
"type": "string"
},
"Age": {
"type": "integer"
},
"Department": {
"type": [
"string",
"null"
],
"default": null
}
},
"required": [
"Name",
"Age"
]
}
You get a JSON-specialized schema of any type. As seen, it provides information about whether a field is nullable or non-nullable and specifies required fields. You can alter the schema response by configuring JsonSerializerOptions
or JsonTypeInfo
instances.
Code example
public class Mobile
{
public int ModelNumber { get; init; }
}
In Program.cs
:
JsonSerializerOptions options = new(JsonSerializerOptions.Default)
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
NumberHandling = JsonNumberHandling.WriteAsString,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
};
JsonNode schema = options.GetJsonSchemaAsNode(typeof(Mobile));
Console.WriteLine(schema.ToString());
Output:
{
"type": [
"object",
"null"
],
"properties": {
"model_number": {
"type": [
"string",
"integer"
],
"pattern": "^-?(?:0|[1-9]\\d*)$"
}
},
"additionalProperties": false
}
Here, I altered JsonSerializationOptions
. In the PropertyNamingPolicy
, I defined the case of the output property name as SnakeCaseLower
, which is model_number
. Other possible values are:
CamelCase
SnakeCaseLower
SnakeCaseUpper
KebabCaseLower
KebabCaseUpper
Secondly NumberHandling = JsonNumberHandling.WriteAsString
, serializes numbers as strings in JSON. ModelNumber
will be represented as a string. However, it will also take numeric data matched with the regex, although it will be input as a string.
Lastly, JsonUnmappedMemberHandling.Disallow
throws an exception if the JSON contains properties not mapped to the Mobile
type.
JsonNode
saves the schema to represent it as a JSON document.
With new updates, you can also customize the schema generation using JsonSchemaExporterOptions
. In the coding example, I will apply Description
attributes to a generated schema for the Student
. Start but including the System.ComponentModel
namespace:
using System.ComponentModel;
The Student
record is simplified for this example:
[Description("A student")]
record Student([property: Description("The name of the student")] string Name);
In Program.cs
:
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Schema;
JsonSchemaExporterOptions exporterOptions = new()
{
TransformSchemaNode = (context, schema) =>
{
// Determine if processing a type or property and retrieving attribute information
ICustomAttributeProvider? attributeProvider = context.PropertyInfo is not null
? context.PropertyInfo.AttributeProvider
: context.TypeInfo.Type;
// Look up any DescriptionAttribute
DescriptionAttribute? descriptionAttr = attributeProvider?
.GetCustomAttributes(inherit: true)
.Select(attr => attr as DescriptionAttribute)
.FirstOrDefault(attr => attr is not null);
// Apply description attribute to the schema
if (descriptionAttr != null)
{
if (schema is not JsonObject jObj)
{
// Handle boolean schema case
JsonValueKind valueKind = schema.GetValueKind();
Debug.Assert(valueKind is JsonValueKind.True or JsonValueKind.False);
schema = jObj = new JsonObject();
if (valueKind is JsonValueKind.False)
{
jObj.Add("not", true);
}
}
jObj.Insert(0, "description", descriptionAttr.Description);
}
return schema;
}
};
JsonSerializerOptions options = JsonSerializerOptions.Default;
JsonNode schema = options.GetJsonSchemaAsNode(typeof(Student), exporterOptions);
Console.WriteLine(schema.ToString());
The TransformSchemaNode
delegate lambda function modifies each schema node and applies DescriptionAttribute
values for the generated JSON. If a DescriptionAttribute
is found, as in our case, it is added to the generated schema.
Web Defaults Serialization Singleton
.NET 9 simplifies web-based serialization with the new JsonSerializerDefaults
singleton. With this new feature, you can align your JSON with commonly used practices in web applications, such as camel case property naming and compatibility with JSON-based APIs.
Code example
var mobile = new Mobile()
{
ModelNumber = 223
};
string json = JsonSerializer.Serialize(
mobile,
JsonSerializerDefaults.Web
);
Console.WriteLine(json);
Customizing Indentation with JsonSerializerOptions
One of the key additions to the library is indentation customization using JsonSerializerOptions
. Previously, any response was not well-indented and readable, and developers may need to do it manually or through a third-party application. Fortunately, you can configure it within the application.
Code example
var options = new JsonSerializerOptions
{
WriteIndented = true,
IndentCharacter = '\t',
IndentSize = 1,
};
string json = JsonSerializer.Serialize(
new
{
Field1 = 1,
Field2 = 2
},
options
);
Console.WriteLine(json);
Output
Or applying on an object
var mobile = new Mobile()
{
ModelNumber = 223
};
var jsonMobile = JsonSerializer.Serialize(mobile, options);
Console.WriteLine(jsonMobile);
Output
Here, I defined the Indentation of size 1 tab for the fields.
Streaming multiple JSON documents
Utf8JsonReader
now allows multiple whitespace-separated JSON documents to be read. Traditionally, you get an exception for trailing non-whitespace characters within a single buffer or stream. You can set the JsonReaderOptions.AllowMultipleValues
flag of JsonReaderOptions
. Consider the following example.
Code example
using System.Text.Json;
JsonReaderOptions options = new() { AllowMultipleValues = true };
Utf8JsonReader reader = new("null {} 7 \r\n [7,8,9]"u8, options);
reader.Read();
Console.WriteLine(reader.TokenType);
reader.Read();
Console.WriteLine(reader.TokenType);
reader.Skip();
reader.Read();
Console.WriteLine(reader.TokenType);
reader.Read();
Console.WriteLine(reader.TokenType);
reader.Skip();
Console.WriteLine(reader.Read());
Output
We keep forwarding the JSON and printing token types.
Customizing enum member names
The library adds the JsonStringEnumMemberName
attribute, which allows you to provide a custom string representation of individual enum members when serializing enums to JSON. This update is useful when you need to specify a context-specific name for enum members.
[Flags, JsonConverter(typeof(JsonStringEnumConverter))]
enum MyEnum
{
Value1 = 1,
[JsonStringEnumMemberName("2nd value")]
Value2 = 2,
}
var result = JsonSerializer.Serialize(MyEnum.Value1 | MyEnum.Value2);
// result will be: "Value1, 2nd value"
The [Flags]
attribute indicates that the enum can be treated as a bit field, a set of flags. It allows combining multiple enum values using a bitwise OR operation (e.g., MyEnum.Value1 | MyEnum.Value2
).
The JsonConverter(typeof(JsonStringEnumConverter))
attribute specifies that this enum should use JsonStringEnumConverter
for serialization. This converter converts enum values to their string representations rather than numeric ones.
JsonObject property order manipulation
Another breakthrough which System.Text.Json
brings with .NET 9 is the order manipulation of JSON objects. The JsonObject
type is a DOM type that represents JSON objects. By default, It has an implicit property order that the user cannot modify. However, It now implements IList<KeyValuePair<string, JsonNode?>>
that extends the JsonObject
instance with the methods
public int IndexOf(string key);
public void Insert(int index, string key, JsonNode? value);
public void RemoveAt(int index);
You can leverage these methods to reorder JSON fields. Let's see how to do it.
Code example
// Creates an ordered JSON object schema from a POCO (Plain Old CLR Object)
var schema = (JsonObject)JsonSerializerOptions.Default.GetJsonSchemaAsNode(typeof(MyPoco));
// Check the index of the "$id" property
switch (schema.IndexOf("$id", out JsonNode? idValue))
{
case < 0: // "$id" property is missing
idValue = (JsonNode)"https://MyApi.com/schema1";
schema.Insert(0, "$id", idValue); // Insert at the start
break;
case 0: // "$id" property is already at the start
break;
case int index: // "$id" exists but is not at the start
schema.RemoveAt(index); // Remove from the current position
schema.Insert(0, "$id", idValue); // Insert at the start
}
Conclusion
serializationSystem.Text.Json
is a robust .NET package that offers JSON operations such as serialization, deserialization, read/write streaming, options configurations, schema validation, and type conversions. With .NET 9, this library also brings many enhancements, including shema export, Web Defaults Serialization Singleton, and multiple JSON document support. In this post, I went through some of the critical features with coding examples. All these features take the package and .NET framework to a new height.
elmah.io: Error logging and Uptime Monitoring for your web apps
This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.
See how we can help you monitor your website for crashes Monitor your website