In this article, we will explore how to serialize a dictionary to JSON, while making use of two of the most popular .NET serialization libraries: Newtonsoft.Json and System.Text.Json.

To download the source code for this article, you can visit our GitHub repository.

So, let’s start.

Serializing a Simple Dictionary

Let’s begin by defining a very simple Dictionary containing an inventory of fruit:

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
var fruitInventory = new Dictionary<string, int>
{
    {"apple", 3},
    {"orange", 5},
    {"banana", 7}
};

Now let’s serialize it to JSON:

JsonConvert.SerializeObject(fruitInventory); // Newtonsoft

JsonSerializer.Serialize(fruitInventory); // System.Text.Json

Here we notice how simple it is to serialize an object using either of the two libraries. Both of them allow us to serialize our dictionary to JSON with just a single line of code.

Now let’s look at the output:

============ Newtonsoft.Json ============
{"apple":3,"orange":5,"banana":7}

============ System.Text.Json ============
{"apple":3,"orange":5,"banana":7}

Both libraries serialize our simple dictionary to JSON, but due to the lack of whitespace, we may find the output a little hard to read. 

Formatting the Output for Readability

While it is great that we can quickly and easily serialize a dictionary with just a single line of code, if we are planning to produce output that will be human-readable, we need to find a way to adjust our output format a bit. Thankfully both libraries provide a mechanism to override the default output settings:

JsonConvert.SerializeObject(fruitInventory, Formatting.Indented);

JsonSerializer.Serialize(fruitInventory, new JsonSerializerOptions
{
    WriteIndented = true
});

We saw that both libraries allow for serialization with a single line of code, but here things have changed a little. The Newtonsoft.Json still allows for serialization with a single line due to the method overload which takes an Formattingenum parameter. System.Text.Json on the other hand, requires us to specify an JsonSerializerOptions object in which we set the WriteIndented property to true.

Now let’s take a look at the updated output:

============ Newtonsoft.Json - Indented ============
{
  "apple": 3,
  "orange": 5,
  "banana": 7
}

============ System.Text.Json - Indented ============
{
  "apple": 3,
  "orange": 5,
  "banana": 7
}

Here we see output that is far easier to read due to the indenting and additional whitespace.

Serializing a Complex Dictionary to JSON

We have already seen how to serialize a simple dictionary, but what happens when our dictionary contains more complex objects? To explore this scenario let’s imagine that we have been tasked with adding a new method to our REST API that returns a collection of customers mapped to a list of their invoices. Now imagine that within our code we already have just such a dictionary where the keys are Customerobjects and the value is a list of Invoice objects. This is a perfect situation in which we would want to serialize a dictionary to JSON.

To explore this example, first, we need to create some records:

public sealed record Person(string FirstName, string LastName)
{
    public override string ToString() => (FirstName + " " + LastName).Trim();
}

public sealed record Customer(string Address, string PhoneNumber, Person Client, Guid CustomerId);

public sealed record Product(string Name, decimal CostPerItem, Guid ProductId);

public sealed record InvoiceLineItem(Product Item, int Quantity);

public sealed record Invoice(DateTime InvoiceDate, Guid InvoiceId, List<InvoiceLineItem> Items);

Now let’s create some sample data and populate a Dictionary:

public static Dictionary<Customer, List<Invoice>> GenerateSampleInvoices()
{
    var customers = new[]
    {
        new Customer(
            "123 Nowhere Street, Fakeville, MD",
            "555-555-0001",
            new Person("John", "Smith"),
            Guid.NewGuid()
        ),
        // Omitted for brevity
    };

    var products = new List<Product>
    {
        new("Widget", 1.27m, Guid.NewGuid()),
        // Omitted for brevity
    };

    var invoices = new List<Invoice>
    {
        new(
            DateTime.Now.AddDays(-14),
            Guid.NewGuid(),
            new List<InvoiceLineItem>
            {
                new(products[0], 10),
                // Omitted for brevity
            }
        ),
        // Omitted for brevity
    };

    var dictionary = new Dictionary<Customer, List<Invoice>>
    {
        {customers[0], invoices.Take(2).ToList()},
        // Omitted for brevity
    };

    return dictionary;
}

Some code has been omitted for brevity, but you can find a complete listing in our source code.

Serializing a Dictionary to JSON with Newtonsoft.Json

Now let’s serialize our dictionary using Newtonsoft.Json:

JsonConvert.SerializeObject(invoicesByCustomer, Formatting.Indented);

And here is the output:

{
  "Customer { Address = 123 Nowhere Street, Fakeville, MD, PhoneNumber = 555-555-0001, 
   Client = John Smith, CustomerId = 470ebbe7-f540-43a2-bc7c-998d703fb5f1 }": [
    {
      "InvoiceDate": "2023-03-20T00:54:13.4187381+02:00",
      "InvoiceId": "7769b342-d7f9-4154-8523-c9077c2ee3f8",
      "Items": [
        {
          "Item": {
            "Name": "Widget",
            "CostPerItem": 1.27,
            "ProductId": "84ccf266-fe7e-4727-a88c-4cf869be371b"
          },
          "Quantity": 10
        },
        {
          "Item": {
            "Name": "Thing",
            "CostPerItem": 35.72,
            "ProductId": "b6b14309-4c3e-4839-ae62-41eff3449af3"
          },
          "Quantity": 10
        }
      ]
    },
   ...

Note: The remainder of the output has been removed for brevity. We will do the same throughout the rest of the article.

Once again we see that with Newtonsoft.Json we can serialize our complex object with a single line of code, but we may not be completely happy with the result. When we look at the keys for the invoice array, we see that the library uses the entire Customer as the key. We can solve this by implementing a custom converter:

public sealed class NewtonsoftCustomerInvoiceConverter : JsonConverter<Dictionary<Customer, List<Invoice>>>
{
    public override void WriteJson(JsonWriter writer, Dictionary<Customer, List<Invoice>>? value,
        JsonSerializer serializer)
    {
        if (value is null) return;

        writer.WriteStartObject();

        foreach (var (customer, invoices) in value)
        {
            writer.WritePropertyName($"Customer-{customer.CustomerId:N}");

            writer.WriteStartObject();

            WriteCustomerProperties(writer, customer);

            WriteInvoices(writer, invoices);

            writer.WriteEndObject();
        }

        writer.WriteEndObject();
    }

    // Omitted for brevity
}

Our main focus is the WriteJson override that allows us to customize how our object is serialized. For more details on creating custom converters, you can check out the article How to Create a Custom JsonConverter in Json.NET.

Let’s take a quick look at what our method is doing. First, we write out the start of our object with a call to StartObject. Next, we iterate through the collection of Customer and Invoice objects, calling our helper methods to write out the appropriate properties. Finally, we close out our JSON with a final call to EndObject.

Now let’s take a look at our helper methods. First, we have WriteCustomerProperties:

private static void WriteCustomerProperties(JsonWriter writer, Customer customer)
{
    writer.WritePropertyName(nameof(customer.Client));
    writer.WriteValue(customer.Client.ToString());

    writer.WritePropertyName(nameof(customer.Address));
    writer.WriteValue(customer.Address);

    writer.WritePropertyName(nameof(customer.PhoneNumber));
    writer.WriteValue(customer.PhoneNumber);
}

This method is fairly straightforward. We just have to write out the key-value pairs for each property of the customer that we wish to serialize.

Next, we have WriteInvoices:

private static void WriteInvoices(JsonWriter writer, List<Invoice> invoices)
{
    writer.WritePropertyName("Invoices");
    var jToken = JToken.FromObject(invoices);
    jToken.WriteTo(writer);
}

With this method, we can take advantage of the Newtonsoft.Json JToken class and use it to generate the JSON for our List<Invoice>.

Now that we have seen the code, let’s watch it in action:

{
  "Customer-bfafbb48f6704e0e87f6d44ac219cf7b": {
    "Client": "John Smith",
    "Address": "123 Nowhere Street, Fakeville, MD",
    "PhoneNumber": "555-555-0001",
    "Invoices": [
      {
        "InvoiceDate": "2023-03-22T22:14:28.2126817+02:00",
        "InvoiceId": "5f14c93e-cc80-492c-9873-0426f6f5cc7f",
        "Items": [
          {
            "Item": {
              "Name": "Widget",
              "CostPerItem": 1.27,
              "ProductId": "127ed2a0-ea95-46b5-8c3a-b0e03d6380d1"
            },
            "Quantity": 10
          },
         ...

Serializing a Dictionary to JSON with System.Text.Json

Let’s go ahead and try serializing our sample data with System.Text.Json:

JsonSerializer.Serialize(invoicesByCustomer,new JsonSerializerOptions
{
    WriteIndented = true,
});

Unfortunately, running this code throws a System.NotSupportedException. So let’s examine what went wrong and how we can fix it.

The exception tells us that the Customer type is not a supported dictionary key type for any of the built-in converters. So how can we solve this issue? We need to create a custom converter.

Creating a Custom Converter for System.Text.Json

We just need to create a custom converter and add it to our serializer options declaration:

public sealed class SystemJsonCustomerInvoiceConverter : JsonConverter<Dictionary<Customer, List<Invoice>>>
{
    public override void Write(Utf8JsonWriter writer, Dictionary<Customer, List<Invoice>> value,
        JsonSerializerOptions options)
    {
        writer.WriteStartObject();

        foreach (var (customer, invoices) in value)
        {
            writer.WritePropertyName($"Customer-{customer.CustomerId:N}");

            writer.WriteStartObject();

            WriteCustomerProperties(writer, customer);

            WriteInvoices(writer, invoices);

            writer.WriteEndObject();
        }

        writer.WriteEndObject();
    }
    // Omitted for brevity
}

This code is very similar to the custom converter we created for Newtonsoft.Jsonserialization. The syntax is slightly different, but the concept is completely the same. First, we call WriteStartObject to begin writing our object. Next, we loop through the collection of Customer and Invoice objects, calling our helper methods to serialize the related properties. Lastly, we close out our object with a call to WriteEndObject.

Now let’s take a look at our helper methods. First, WriteCustomerProperties:

private static void WriteCustomerProperties(Utf8JsonWriter writer, Customer customer)
{
    writer.WritePropertyName(nameof(customer.Client));
    writer.WriteStringValue(customer.Client.ToString());

    writer.WritePropertyName(nameof(customer.Address));
    writer.WriteStringValue(customer.Address);

    writer.WritePropertyName(nameof(customer.PhoneNumber));
    writer.WriteStringValue(customer.PhoneNumber);
}

As we would expect, our code is nearly identical to the code we wrote for our Newtonsoft custom converter.

Next up we have our WriteInvoices method:

private static void WriteInvoices(Utf8JsonWriter writer, List<Invoice> invoices)
{
    writer.WritePropertyName("Invoices");
    writer.WriteStartArray();

    foreach (var invoice in invoices) WriteInvoiceProperties(writer, invoice);

    writer.WriteEndArray();
}

This time our WriteInvoices code is not as succinct as we don’t have a JToken class to help us so we have to serialize the list ourselves. We start by writing our key, but because we are serializing a List we have to serialize it as an array.

Once we have started the array, we simply iterate over the items and write them out using our helper method WriteInvoiceProperties:

private static void WriteInvoiceProperties(Utf8JsonWriter writer, Invoice invoice)
{
    writer.WriteStartObject();

    writer.WritePropertyName(nameof(invoice.InvoiceDate));
    writer.WriteStringValue(invoice.InvoiceDate);

    writer.WritePropertyName(nameof(invoice.InvoiceId));
    writer.WriteStringValue(invoice.InvoiceId);

    writer.WritePropertyName(nameof(invoice.Items));
    writer.WriteStartArray();
    foreach (var lineItem in invoice.Items)
    {
        writer.WriteStartObject();

        WriteLineItem(writer, lineItem);

        writer.WriteEndObject();
    }
    writer.WriteEndArray();

    writer.WriteEndObject();
}

Since each Invoice is itself an object, we start with WriteStartObject and end with WriteEndObject. After starting our object we begin writing the key-value pairs. When we get to the Itemsproperty, however, since it is a List, we have to serialize it as an array. Once again we make use of a helper method, WriteLineItem:

private static void WriteLineItem(Utf8JsonWriter writer, InvoiceLineItem lineItem)
{
    writer.WritePropertyName("Item");

    WriteProductItem(writer, lineItem.Item);

    writer.WritePropertyName(nameof(lineItem.Quantity));
    writer.WriteNumberValue(lineItem.Quantity);
}

This code follows the same pattern we have already seen, namely writing key-value pairs. We also call one last helper method to write the Product object:

private static void WriteProductItem(Utf8JsonWriter writer, Product productItem)
{
    writer.WriteStartObject();

    writer.WritePropertyName(nameof(productItem.Name));
    writer.WriteStringValue(productItem.Name);

    writer.WritePropertyName(nameof(productItem.CostPerItem));
    writer.WriteNumberValue(productItem.CostPerItem);

    writer.WritePropertyName(nameof(productItem.ProductId));
    writer.WriteStringValue(productItem.ProductId);

    writer.WriteEndObject();
}

Now that we have our custom converter created, let’s try our serialization again:

var systemJsonComplex = JsonSerializer.Serialize(invoicesByCustomer,new JsonSerializerOptions
{
    Converters = { new CustomerInvoiceConverter() },
    WriteIndented = true,
});

This time our code runs without any exceptions and we see our expected output:

{
  "Customer-c6caed9431904e609e6dfdd43955bc0b": {
    "Client": "John Smith",
    "Address": "123 Nowhere Street, Fakeville, MD",
    "PhoneNumber": "555-555-0001",
    "Invoices": [
      {
        "InvoiceDate": "2023-03-22T22:16:48.1286004+02:00",
        "InvoiceId": "ee47065c-6f98-4546-a58f-b82e5460f513",
        "Items": [
          {
            "Item": {
              "Name": "Widget",
              "CostPerItem": 1.27,
              "ProductId": "eedb7fd3-b0d7-441f-9076-a789d1918311"
            },
            "Quantity": 10
          },
          ...

Conclusion

In this article, we have seen how we can serialize both a simple and complex dictionary to JSON using two of the most popular JSON libraries: Newtonsoft.Json and System.Text.Json. We have also seen how to create custom converters which enable us to have tighter control over how objects are serialized.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!