System.Text.Json can’t serialize Dictionary unless it has a string key

Before .NET 5, the built-in JSON serializer (System.Text.Json) couldn’t handle serializing a dictionary unless it had a string key. Here’s an example running in .NET Core 3.1 to show the problem. This is initializing a dictionary with values and then attempting to serialize it.

var map = new Dictionary<int, string>()
{
	{ 1, "hello" },
	{ 2, "world" }
};

Console.Write(System.Text.Json.JsonSerializer.Serialize(map));
Code language: C# (cs)

This results in exception:

System.NotSupportedException: The collection type ‘System.Collections.Generic.Dictionary`2[System.Int32,System.String]’ is not supported.

This wasn’t really a bug, just a missing feature in System.Text.Json. They addressed this in .NET 5, and you can now use System.Text.Json to serialize dictionaries with non-string keys. This is still a problem if you need to use an older version (like .NET Core 3.1). So I’ll discuss a few options.

Solution

There are three options for solving this:

  • Use Newtonsoft for handling this specific case.
  • Write a custom converter.
  • Use System.Text.Json version 5+ (it can be used in older versions of .NET!).

I’ll discuss the details below.

Option 1 – Use Newtonsoft

The simple solution is to use Newtonsoft, because it already supports serialization of any dictionaries.

  • Install the Newtonsoft.Json package (View > Other Windows > Package Manager).
Install-Package Newtonsoft.Json
Code language: PowerShell (powershell)
  • Change your code to use Newtonsoft.
var map = new Dictionary<int, string>()
{
	{ 1, "hello" },
	{ 2, "world" }
};

Console.Write(Newtonsoft.Json.JsonConvert.SerializeObject(map));
Code language: C# (cs)

Option 2 – Write a custom dictionary converter

You can write a custom JSON converter to handle any serialization / deserialization scenario that isn’t handled by the built-in functionality.

1 – Implement a custom converter

To create a custom converter you need to inherit from JsonConverter, then override three methods: CanConvert(), Read(), and Write().

I created the following custom dictionary converter that handles dictionaries with any key type, as long as that type is convertible from a string. It loops through the dictionary you’re trying to convert, converts the keys to strings, and then adds to the new dictionary.

public class CustomDictionaryJsonConverter<TKey, TValue> : JsonConverter<IDictionary<TKey, TValue>> where TKey : IConvertible
{
	public override bool CanConvert(Type typeToConvert)
	{
		/* Only use this converter if 
		 * 1. It's a dictionary
		 * 2. The key is not a string
		 */
		if (typeToConvert != typeof(Dictionary<TKey, TValue>))
		{
			return false;
		}
		else if (typeToConvert.GenericTypeArguments.First() == typeof(string))
		{
			return false;
		}
		return true;
	}
	public override IDictionary<TKey, TValue> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
	{
		//Step 1 - Use built-in serializer to deserialize into a dictionary with string key
		var dictionaryWithStringKey = (Dictionary<string, TValue>)JsonSerializer.Deserialize(ref reader, typeof(Dictionary<string, TValue>), options);


		//Step 2 - Convert the dictionary to one that uses the actual key type we want
		var dictionary = new Dictionary<TKey, TValue>();

		foreach (var kvp in dictionaryWithStringKey)
		{
			dictionary.Add((TKey)Convert.ChangeType(kvp.Key, typeof(TKey)), kvp.Value);
		}

		return dictionary;
	}

	public override void Write(Utf8JsonWriter writer, IDictionary<TKey, TValue> value, JsonSerializerOptions options)
	{
		//Step 1 - Convert dictionary to a dictionary with string key
		var dictionary = new Dictionary<string, TValue>(value.Count);

		foreach (var kvp in value)
		{
			dictionary.Add(kvp.Key.ToString(), kvp.Value);
		}
		//Step 2 - Use the built-in serializer, because it can handle dictionaries with string keys
		JsonSerializer.Serialize(writer, dictionary, options);

	}
}
Code language: C# (cs)

Note: If your type cannot be converted to string, think about implementing IConvertible.

I tried to write this with code readability in mind, instead of trying to make it super generalized. The downside if you have to create an instance of this converter for each specific type of dictionary you are using. Ex: if you need to serialize Dictionary<int, string> and Dictionary<DateTime, string>, you’ll need two instances of the converter.

2 – Use the custom dictionary converter

To use the custom dictionary converter, you need to pass in an instance of the converter to JsonSerializerOptions. Then when you call Deserialize() or Serialize(), it’ll use the custom converter if CanConvert() is true.

using System.Text.Json;

static void Main(string[] args)
{
	Dictionary<int, string> map = new Dictionary<int, string>()
	{
		{ 1, "hello" },
		{ 2, "world" }
	};

	JsonSerializerOptions options = new JsonSerializerOptions();
	options.Converters.Add(new CustomDictionaryJsonConverter<int, string>());

	var json = JsonSerializer.Serialize(map, options);

	Console.WriteLine(json);

	var deserializedMap = JsonSerializer.Deserialize<Dictionary<int, string>>(json, options);

	Console.WriteLine(JsonSerializer.Serialize(deserializedMap, options));
}
Code language: C# (cs)

3 – Run it

When I run this, it correctly serializes and deserializes the Dictionary<int, string>.

Serialized with custom dictionary converter
{ "1":"hello", "2":"world"}

Deserialized, then serialized, with custom dictionary converter
{"1":"hello","2":"world"}Code language: plaintext (plaintext)

Option 3 – Use System.Text.Json package v5+

The serialize dictionaries with non-string key functionality was added in System.Text.Json v5. You can get this functionality by installing System.Text.Json package v5 or above. This works in multiple versions of .NET, including older versions.

So first install it (this is installing the latest version with Views > Other Windows > Package Manager):

Install-Package System.Text.Json
Code language: PowerShell (powershell)

Then use the functionality. The following code is running in .NET Core 3.1 and using the latest System.Text.Json package (v7 at the time of writing). Previously, this would result in an exception. With System.Text.Json package v5+, this works without a problem:

//Using System.Text.Json v7 in a .NET Core 3.1 project

var map = new Dictionary<int, string>()
{
	{ 1, "hello" },
	{ 2, "world" }
};

Console.Write(System.Text.Json.JsonSerializer.Serialize(map));
Code language: C# (cs)

Leave a Comment