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.
So, let’s start.
Serializing a Simple Dictionary
Let’s begin by defining a very simple Dictionary
containing an inventory of fruit:
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 Formatting
enum 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 Customer
objects 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.Json
serialization. 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 Items
property, 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.