In this article, we are going to learn how to perform property ordering within JSON serialization using the two most famous JSON libraries for .NET. The libraries are System.Text.Json, which has been natively available in the framework since .NET Core 3.0, and Newtonsoft.Json.
Let’s start.
What is JSON Property Ordering?
JSON property ordering is the arrangement of an object’s properties in JSON output. While the order of properties may not be significant in most cases, it can be crucial when working with APIs that require a specific order.
Fortunately, both System.Text.Json and Newtonsoft.Json provide the ability to specify a custom order for properties in JSON. To learn more about how to serialize objects, we can check out our article How to Turn a C# Object Into a JSON String in .NET.
For this article, we are going to use a simple console application in .NET 7 version and install the Newtonsoft.Json
library through the NuGet Package Manager Console
:
PM> Install-Package Newtonsoft.Json
To print formatted results, we are going to use the WriteIndented
option from System.Text.Json
and Formatting.Indented
from the Newtonsoft.Json
library:
JsonSerializer.Serialize(..., new JsonSerializerOptions { WriteIndented = true } ); JsonConvert.SerializeObject(..., Formatting.Indented );
Default Property Serialization Order
When serializing an object using System.Text.Json
or Newtonsoft.Json
, both libraries use reflection via the GetProperties
method. In this case, the method first returns the properties of the derived class. If using inheritance, it will return the properties of the inherited classes in hierarchical order. It should be noted that from .NET 7 onward, the properties are returned in the order they appear in the assembly’s metadata:
The MetadataToken
is an integer value assigned to each member of a type by the runtime, and reflects the position of the member within the metadata of the assembly so that the properties with lower MetadataToken
values are returned first.
We can illustrate this behavior through an example. Let’s create a Person
class and a Student
derived class:
public class Person { public int Id { get; set; } public string? Name { get; set; } } public class Student: Person { public int RegistratioNumber { get; set; } public double Grade { get; set; } }
Now let’s get the properties through reflection using the GetProperties
method:
var properties = typeof(Student) .GetProperties(BindingFlags.Public | BindingFlags.Instance) .ToList(); foreach (var property in properties) { Console.WriteLine($"{property.Name} {property.MetadataToken}"); }
And, let’s check the output:
RegistratioNumber 385875971 Grade 385875972 Id 385875969 Name 385875970
As we can see, the MetadataToken
values are generated according to the order of the properties of each class, first returning the properties of the derived class and proceeding upward through its inheritance hierarchy.
Order Using Property Attribute
To modify the default JSON serialization order of the System.Text.Json
library, we can utilize the JsonPropertyOrder
attribute on class properties that require a different order.
It’s worth noting that any property that lacks an order setting will receive a value of zero (0). Hence, if we wish to order a property first by using a positive value, we must assign a higher order to all other serialized properties. Alternatively, we can set the initial property’s order to -1.
Let’s create a Vehicle
class and a Car
derived class:
public class Vehicle { [JsonPropertyOrder(-1)] public int Id { get; set; } public string? Manufacturer { get; set; } } public class Car: Vehicle { public int NumberOfDoors { get; set; } }
In our Program
class, let’s serialize the Car
object through the System.Text.Json
library:
var car = new Car() { NumberOfDoors = 4, Manufacturer = "Fiat", Id = 1 }; var json = JsonSerializer.Serialize(car, new JsonSerializerOptions { WriteIndented = true } ); Console.WriteLine(json);
In the output, we have our object serialized, but with the Id
property coming first:
{ "Id": 1, "NumberOfDoors": 4, "Manufacturer": "Fiat" }
To modify the serialization order via the Newtonsoft.Json
library, we can utilize the [JsonProperty]
attribute decorator from the Newtonsoft.Json
namespace and specify the Order as -2. This is necessary because the default value, in this case, is -1:
public class Vehicle { [JsonProperty(Order = -2)] public int Id { get; set; } public string? Manufacturer { get; set; } }
To serialize the object, let’s run the code:
var json = JsonConvert.SerializeObject(car, Formatting.Indented ); Console.WriteLine(json);
As with the serialization with the previous library, our serialized object has had its default property ordering changed:
{ "Id": 1, "NumberOfDoors": 4, "Manufacturer": "Fiat" }
Order Using JsonConverter
In System.Text.Json
, JsonConverter
is a class that can be used to customize the serialization and deserialization of a type. A JsonConverter
can be used to control how an object is converted to a JSON string and vice versa.
Let’s create a JsonConverter
that alphabetically sorts the properties of a class:
public class MicrosoftOrderedPropertiesConverter<T> : JsonConverter<T> { public override bool CanConvert(Type typeToConvert) { return typeof(T).IsAssignableFrom(typeToConvert); } public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return JsonSerializer.Deserialize<T>(ref reader, options); } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { writer.WriteStartObject(); var properties = typeof(T).GetProperties().OrderBy(p => p.Name).ToList(); foreach (var property in properties) { var propertyValue = property.GetValue(value); writer.WritePropertyName(property.Name); JsonSerializer.Serialize(writer, propertyValue, options); } writer.WriteEndObject(); } }
Through the Utf8JsonWriter
, we start by calling WriteStartObject
to start a new object. This method writes the opening brace character {
to the output stream and sets the internal state of the writer to indicate that an object is being written.
After calling WriteStartObject
, we use reflection to get the properties of an object at runtime by Type.GetProperties
method. This method returns an array of PropertyInfo
objects, each of which represents a property of the object, and then we use LINQ
to order the properties by name.
Then, after ordering the properties, we write the object properties using WritePropertyName
and use JsonSerializer.Serialize
to output the property value.
After writing all the object properties, we call WriteEndObject
to write the closing brace character }
to the output stream and reset the internal state of the writer.
To demonstrate its use, let’s create anAnimal
class and add the MicrosoftOrderedPropertiesConverter
as an attribute class:
[JsonConverter(typeof(MicrosoftOrderedPropertiesConverter<Animal>))] public class Animal { public int Id { get; set; } public string? Name { get; set; } public int Age { get; set; } }
And then we can serialize the Animal
object:
var animal = new Animal() { Id = 1, Name = "Miau", Age = 3 }; var json = JsonSerializer.Serialize(animal, new JsonSerializerOptions { WriteIndented = true } ); Console.WriteLine(json);
The resulting JSON is:
{ "Age": 3, "Id": 1, "Name": "Miau" }
We can also register the converter through JsonSerializerOptions:
var options = new JsonSerializerOptions { WriteIndented = true }; options.Converters.Add(new MicrosoftOrderedPropertiesConverter<Animal>()); var json = JsonSerializer.Serialize(animal, options); Console.WriteLine(json);
We can do the same in Newtosoft.Json
, but with a few differences. Now, let’s show the same behavior using the JsonConverter
from this library:
public class NewtonsoftOrderedPropertiesConverter<T> : JsonConverter<T> { public override T? ReadJson(JsonReader reader, Type objectType, T? existingValue, bool hasExistingValue, JsonSerializer serializer) { if (!hasExistingValue) { existingValue = Activator.CreateInstance<T>(); } serializer.Populate(reader, existingValue!); return existingValue; } public override void WriteJson(JsonWriter writer, T? value, JsonSerializer serializer) { writer.WriteStartObject(); var properties = typeof(T).GetProperties().OrderBy(p => p.Name); foreach (var property in properties) { var propertyValue = property.GetValue(value); writer.WritePropertyName(property.Name); serializer.Serialize(writer, propertyValue); } writer.WriteEndObject(); } }
Now, we need to pass the custom NewtonsoftOrderedPropertiesConverter
to the JsonSerializerSettings
:
var json = JsonConvert.SerializeObject(animal, Formatting.Indented, new NewtonsoftOrderedPropertiesConverter<Animal>() ); Console.WriteLine(json);
The result is the same:
{ "Age": 3, "Id": 1, "Name": "Miau" }
Order Using IContractResolver
By default, Newtonsoft.Json
uses the DefaultContractResolver
, which implements the IContractResolver
interface and provides a set of default serialization contracts for the most common types. However, we can create our own custom contract resolver by implementing the IContractResolver
interface. This approach enables us to define custom serialization contracts tailored to specific types or members.
For our case, we can sort the properties through the override of DefaultContractResolver
:
public class OrderedPropertiesContractResolver : DefaultContractResolver { protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization) { var properties = base.CreateProperties(type, memberSerialization); return properties.OrderBy(p => p.PropertyName).ToList(); } }
And then, we can use it by instantiating it in the ContractResolver
property of the JsonSerializerSettings
class:
var json = JsonConvert.SerializeObject(animal, Formatting.Indented, new JsonSerializerSettings { ContractResolver = new OrderedPropertiesContractResolver() } ); Console.WriteLine(json);
And the output is:
{ "Age": 3, "Id": 1, "Name": "Miau" }
Order Using IJsonTypeInfoResolver
In previous versions, System.Text.Json
only allowed us to make limited tweaks to the contract, just by annotating the System.Text.Json
attribute.
Starting with .NET 7, users can write their own JSON contract resolution logic using implementations of the IJsonTypeInfoResolver
interface. Contract resolution performed by the default serializer is exposed through the DefaultJsonTypeInfoResolver
class, which implements IJsonTypeInfoResolver
.
Let’s create a OrderedPropertiesJsonTypeInfoResolver
class that overrides DefaultJsonTypeInfoResolver
to sort the properties:
public class OrderedPropertiesJsonTypeInfoResolver: DefaultJsonTypeInfoResolver { public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options) { var order = 0; JsonTypeInfo typeInfo = base.GetTypeInfo(type, options); if (typeInfo.Kind == JsonTypeInfoKind.Object) { foreach (JsonPropertyInfo property in typeInfo.Properties.OrderBy(a => a.Name)) { property.Order = order++; } } return typeInfo; } }
We override the GetTypeInfo
method, which is called by the JsonSerializer
to get information about a type being serialized or deserialized.
Inside the method, we first call the base implementation to get the default JsonTypeInfo
object for the given type.
If it is an object, we get the collection of JsonPropertyInfo
objects that provide information about each property in an object. Then, we sort by name using the OrderBy
method and assign a unique order to each property by setting the Order
property.
We can use this class by instantiating and setting it to the TypeInfoResolver
property of the JsonSerializerOptions
class:
var json = JsonSerializer.Serialize(instance, new JsonSerializerOptions { WriteIndented = true, TypeInfoResolver = new OrderedPropertiesJsonTypeInfoResolver() } ); Console.WriteLine(json);
And the result is:
{ "Age": 3, "Id": 1, "Name": "Miau" }
Performance
To make a quick comparison between the two libraries, let’s run a benchmark comparing the different property ordering methods that we describe in this article.
Let’s run the benchmark available in the repository of this article:
BenchmarkDotNet.Running.BenchmarkRunner.Run<OrderBenchmark>();
After running the benchmark we can inspect the result:
| Method | Mean | Error | StdDev | Median | |------------------------------------- |-------------:|------------:|-------------:|-------------:| | PropertyOrderUsingSystemTextJson | 258.3 ns | 5.22 ns | 15.16 ns | 256.3 ns | | PropertyOrderUsingNewtonsoftJson | 520.2 ns | 14.48 ns | 42.70 ns | 505.4 ns | | PropertyConverterUsingSystemTextJson | 11,583.2 ns | 226.40 ns | 378.26 ns | 11,583.0 ns | | PropertyConverterUsingNewtonsoftJson | 1,278.6 ns | 25.47 ns | 53.73 ns | 1,278.6 ns | | TypeInfoResolverUsingSystemTextJson | 20,136.5 ns | 393.60 ns | 624.29 ns | 20,142.7 ns | | ContractResolverUsingNewtonsoftJson | 409,177.2 ns | 7,826.88 ns | 14,507.62 ns | 406,158.3 ns |
Analyzing these benchmark results, we may notice that we have a difference between the use of the ordering properties of the libraries, with System.Text.Json
being faster than Newtonsoft.Json
.
On the other hand, when we look at the use of JsonConverter
, we see a large advantage of Newtonsoft.Json
over System.Text.Json
, even though both have practically identical implementations.
Finally, we compare the conceptually similar type and contract resolvers. As we can see, using the resolver provided by System.Text.Json
greatly outperforms its competitor.
Conclusion
Throughout this article, we’ve covered the property ordering process for the System.Text.Json
and Newtonsoft.Json
libraries. We’ve also described how to order properties individually, use converters, and implement a custom contract.
Neither of the libraries can claim the best performance across the board but System.Text.Json
is better in more use cases according to our benchmarking. It’s important to note that each library does have minor differences in implementation, but both are equally capable, generally speaking.