Improving Performance by Reusing JsonSerializerOptions in C#

By FoxLearn 2/4/2025 3:51:41 AM   71
When dealing with object serialization in C#, the System.Text.Json library is often used for converting objects to JSON and vice versa.

However, there is a common performance pitfall when serializing objects: creating a new JsonSerializerOptions object every time. This practice can negatively affect performance, especially when serializing multiple objects of the same type. Fortunately, reusing JsonSerializerOptions can drastically improve performance by caching type information, reducing overhead.

Why Reuse JsonSerializerOptions?

The primary advantage of reusing a JsonSerializerOptions object is the caching of type metadata. When you serialize an object for the first time, the type information has to be generated, which can be time-consuming. However, when you reuse the same JsonSerializerOptions, this type information is cached, allowing the serializer to avoid unnecessary reprocessing in subsequent serializations.

By reusing JsonSerializerOptions, we can see a substantial reduction in the time it takes to serialize the same types multiple times.

Performance Test Setup

To demonstrate the difference in performance, we will compare two approaches:

  • Approach 1: Creating a new JsonSerializerOptions object for each serialization operation.
  • Approach 2: Reusing the same JsonSerializerOptions object for all serialization operations.

We will serialize 100 objects and compare the average serialization times, excluding the first run (which includes initialization overhead).

For this example, we’ll use a sample Product object, which contains information like product name, category, price, and related reviews.

Approach 1: Creating a New JsonSerializerOptions Each Time

In this approach, we create a new JsonSerializerOptions object each time we serialize a Product object. This forces the serializer to reprocess the type metadata for every single object, resulting in higher overhead.

using System.Text.Json;
using System.Diagnostics;

var products = new List<Product>
{
    new Product { Name = "Laptop", Category = "Electronics", Price = 999.99, Reviews = 150 },
    new Product { Name = "Smartphone", Category = "Electronics", Price = 799.99, Reviews = 300 },
    // Add more products as needed
};

List<double> nonCachingOptionTimes = new List<double>();
List<double> timeForCreatingNewOptions = new List<double>();
Stopwatch sw = new Stopwatch();

for (int i = 0; i < 100; i++)
{
    sw.Restart();
    var options = new JsonSerializerOptions() { WriteIndented = true };
    options.Converters.Add(new JsonStringEnumConverter());
    timeForCreatingNewOptions.Add(sw.Elapsed.TotalMilliseconds);
    
    sw.Restart();
    var json = JsonSerializer.Serialize(products, options);
    sw.Stop();
    nonCachingOptionTimes.Add(sw.Elapsed.TotalMilliseconds);
}

Console.WriteLine($"No caching - new options. min={timeForCreatingNewOptions.Min()} max={timeForCreatingNewOptions.Max()} avg={timeForCreatingNewOptions.Average()}");
Console.WriteLine($"No caching - serializing. first={nonCachingOptionTimes.First()} min={nonCachingOptionTimes.Min()} max={nonCachingOptionTimes.Max()} avg={nonCachingOptionTimes.Average()} avgWithoutFirst={nonCachingOptionTimes.Skip(1).Average()}");

Performance Results:

  • Creating New Options:

    • Min: 0.004 ms
    • Max: 2.315 ms
    • Avg: 0.022 ms
  • Serializing:

    • First: 47.204 ms
    • Min: 3.129 ms
    • Max: 47.227 ms
    • Avg: 4.723 ms
    • Avg without First: 4.210 ms

Total time (creating new options + serializing, average without first): 4.232 ms

Approach 2: Reusing JsonSerializerOptions

In this approach, we create a single JsonSerializerOptions object and reuse it for all 100 serialization operations. This avoids the overhead of creating new objects and regenerating type metadata, leading to faster serializations.

using System.Text.Json;
using System.Diagnostics;

var cachedOption = new JsonSerializerOptions() { WriteIndented = true };
cachedOption.Converters.Add(new JsonStringEnumConverter());
List<double> cachedOptionTimes = new List<double>();
Stopwatch sw = new Stopwatch();

for (int i = 0; i < 100; i++)
{
    sw.Restart();
    var json = JsonSerializer.Serialize(products, cachedOption);
    sw.Stop();
    cachedOptionTimes.Add(sw.Elapsed.TotalMilliseconds);
}

Console.WriteLine($"Caching. first={cachedOptionTimes.First()} min={cachedOptionTimes.Min()} max={cachedOptionTimes.Max()} avg={cachedOptionTimes.Average()} avgWithoutFirst={cachedOptionTimes.Skip(1).Average()}");

Performance Results:

  • Serializing:
    • First: 50.002 ms

    • Min: 0.015 ms

    • Max: 50.015 ms

    • Avg: 0.557 ms

    • Avg without First: 0.0145 ms

Performance Comparison

Approach

Average Serialization Time (ms)

Not Reusing JsonSerializerOptions

4.232 ms

Reusing JsonSerializerOptions

0.0145 ms

Reusing JsonSerializerOptions results in a massive performance improvement. In our example, reusing the same JsonSerializerOptions object made serialization over 290x faster than creating a new one for every operation. The majority of the speedup comes from the caching of type metadata, which avoids repeated processing during serialization.