---
title: "A (performance) quirk with JsonSerializer"
description: "`System.Text.Json.JsonSerializer` has a weird quirk in regard to performance and memory management. So we will discuss what is \"wrong\" with this code: `JsonSerializer.Serialize(myObject, new JsonSerializerOptions(...));`."
updated: 2023-02-15
tags: ["C#", ".NET"]
canonical: "https://steven-giesel.com/blogPost/0e7512b0-c854-4cce-8606-765fcaf1e69c/a-performance-quirk-with-jsonserializer"
---

# A (performance) quirk with JsonSerializer

`System.Text.Json.JsonSerializer` has a weird quirk in regard to performance and memory management. So we will discuss what is "wrong" with this code: `JsonSerializer.Serialize(myObject, new JsonSerializerOptions(...));`.

## `JsonSerializerOptions`
The [`JsonSerializerOptions`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions?view=net-7.0) is used to customize how the [`JsonSerializer`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializer?view=net-7.0) serializes (your C# object to a string for example) and deserializes objects (string back to your C# object). It basically consists of a few properties that describe, for example, whether or not you want to have indentation (so whitespaces to make it easier to read for humans, but takes more space) and so on. Now what does it have to do with the code snippet above? Let's have a small benchmark:

## Benchmark
```csharp
[MemoryDiagnoser]
public class JsonBenchmark
{
    private const int Iterations = 100;

    private static readonly JsonSerializerOptions _options = new();

    [Benchmark(Baseline = true)]
    public void SerializeDefault()
    {
        for (var i = 0; i < Iterations; i++)
            JsonSerializer.Serialize(new Test("Steven", "Giesel"));
    }

    [Benchmark]
    public void SerializeOptionsField()
    {
        for (var i = 0; i < Iterations; i++)
            JsonSerializer.Serialize(new Test("Steven", "Giesel"), _options);
    }

    [Benchmark]
    public void SerializeOptionsNew()
    {
        for (var i = 0; i < Iterations; i++)
            JsonSerializer.Serialize(new Test("Steven", "Giesel"), new JsonSerializerOptions());
    }
}

public sealed record Test(string FirstName, string LastName);
```

Results:
```csharp
|                Method |     Mean |    Error |   StdDev | Ratio | RatioSD |   Gen0 |   Gen1 |   Gen2 | Allocated | Alloc Ratio |
|---------------------- |---------:|---------:|---------:|------:|--------:|-------:|-------:|-------:|----------:|------------:|
|      SerializeDefault | 13.89 us | 0.083 us | 0.074 us |  1.00 |    0.00 | 2.2888 |      - |      - |  14.06 KB |        1.00 |
| SerializeOptionsField | 13.89 us | 0.032 us | 0.030 us |  1.00 |    0.01 | 2.2888 |      - |      - |  14.06 KB |        1.00 |
|   SerializeOptionsNew | 53.36 us | 0.781 us | 0.692 us |  3.84 |    0.05 | 6.7139 | 0.2441 | 0.1221 |  42.79 KB |        3.04 |
```

We can see that if you `new` up an instance in a loop, it can get costly. And you might think: Of course - the `new()` itself takes time and memory. Yes and no. Yes, that is true, but the no not that huge amount we can see here. Keep in mind I only used 100 iterations. No there is a complete different reason:

## Caching
The way `Serialize` and `Deserialize` works are as follows:
```csharp
public static string Serialize<TValue>(TValue value, JsonSerializerOptions? options = null)
{
    JsonTypeInfo<TValue> jsonTypeInfo = GetTypeInfo<TValue>(options);
    return WriteString(value, jsonTypeInfo);
}
```

If we dig deeper:
```csharp
private static JsonTypeInfo GetTypeInfo(JsonSerializerOptions? options, Type inputType)
{
    Debug.Assert(inputType != null);

    options ??= JsonSerializerOptions.Default;

    if (!options.IsInitializedForReflectionSerializer)
    {
        options.InitializeForReflectionSerializer();
    }
```
If `options` is not set (like in our baseline case) it uses a default instance. If we pass in our static field it behaves roughly the same. So basically `options.IsInitializedForReflectionSerializer` this will evaluate to true in those two cases and we don't have to go into `InitializeForReflectionSerializer` a second time. Only in our first call.

And here is the difference to the last case (`new()`), because here we will call the `InitializeForReflectionSerializer` every single time. This function is quite expensive as it builds up the necessary information on how your object looks like via reflection. That is where the cost comes from.

## Should I care?
You can basically ignore all this if you don't pass in your own `JsonSerializerOptions`. Also if you call that method only once or a few times you can ignore it to some extent. But you can use `JsonSerializer.Serialize` in combination with Entity Framework in combination with `ValueConverter`:
```csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<MyModel>()
        .Property(e => e.MyList)
        .HasConversion(
            v => JsonSerializer.Serialize(v, new JsonSerializerOptions(...))
            v => JsonSerializer.Deserialize<List<MyType>>(v, new JsonSerializerOptions(...)));
}
```

If you have thousands of rows, that can be very expensive! Better to take either the default (so pass in `null`) or have a static field holding that information for you.