Skip to content

System.Text.Json doesn't support KeyedCollection when serializing with source generation #97617

@Septercius

Description

@Septercius

Description

I'm trying to use the source generation capabilities of System.Text.Json to serialize and deserialize a type that contains an instance of the KeyedCollection<,> class. The documentation (https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/supported-collection-types) states that this is not a fully supported type (specifically, non-string key types), but it does appear to work without problem when not using source generation (so long as I use JsonObjectCreationHandling.Populate), and I get the expected results.

I then tried using the source generation using the same object, and this is failing with a "The given key '0' was not present in the dictionary" error. Looking at the generated code, it's iterating through the collection using a standard for loop, and accessing the items in the collection by index, which doesn't work with KeyedCollection<,> as the indexer requires a key.

Deserializing using source generation does seem to work (it doesn't throw an exception).

A few questions:

  1. Is KeyedCollection<,> actually supported when not using source generation (contrary to what the documentation states)?
  2. Is there any way of getting source generation to use foreach instead?

Reproduction Steps

Paste the following into a new project:

using System;
using System.Text.Json;
using System.Collections.ObjectModel;
using System.Text.Json.Serialization;

namespace STJTest;

class Program
{

	static void Main(string[] args)
	{
		var employeeBob = new Employee() { Id = 1, Name = "Bob" };
		var employeeJane = new Employee() { Id = 2, Name = "Jane" };

		var department = new Department() { Id = 1 };
		department.Employees.Add(employeeBob);
		department.Employees.Add(employeeJane);
		
		// Serialize without the serializer context.
		var serializedWithoutContext = JsonSerializer.Serialize(department);

		Console.WriteLine("Serialized without context:");
		Console.WriteLine(serializedWithoutContext);

		var jsonText = "{\"Id\":2,\"Employees\":[{\"Id\":3,\"Name\":\"Sally\"},{\"Id\":2,\"Name\":\"Gordon\"}]}";

		// Deserialize without the serializer context.
		var deserializedWithoutContext = (Department) JsonSerializer.Deserialize(jsonText, typeof(Department));

		Console.WriteLine("Deserialized without context:");
		foreach (var employee in deserializedWithoutContext.Employees)
		{
			Console.WriteLine("{0} - {1}", employee.Id, employee.Name);
		}

		// Serialize with the context.  This will fail with "The given key '0' was not present in the dictionary".
		var serializedWithContext = JsonSerializer.Serialize(department, typeof(Department), DepartmentContext.Default);

		Console.WriteLine("Serialized with context:");
		Console.WriteLine(serializedWithContext);

		// Deserialize with the context.  This will work correctly.
		var deserializedWithContext = (Department) JsonSerializer.Deserialize(jsonText, typeof(Department), DepartmentContext.Default);

		Console.WriteLine("Deserialized with context:");
		foreach (var employee in deserializedWithContext.Employees)
		{
			Console.WriteLine("{0} - {1}", employee.Id, employee.Name);
		}

		Console.ReadKey();
	}
}


[JsonSerializable(typeof(Department))]
internal partial class DepartmentContext : JsonSerializerContext
{
}

public class Department
{
	public int Id {	get; set; }
	
	[JsonObjectCreationHandling(JsonObjectCreationHandling.Populate)]
	public EmployeeCollection Employees { get; private set; }
	
	public Department()
	{
		Employees = new EmployeeCollection();
	}
}

public class Employee
{
	public int Id {	get; set; }
	public string Name { get; set; }
}


public class EmployeeCollection : KeyedCollection<int, Employee>
{
	protected override int GetKeyForItem(Employee item)
	{
		return item.Id;
	}
}

Expected behavior

The object is successfully serialized to JSON.

Actual behavior

The code throws an exception: "The given key '0' was not present in the dictionary".

Regression?

No response

Known Workarounds

No response

Configuration

.NET 8, Mac OS Ventura 13.6.4, x86_64

Other information

The code generating the source appears to be the GenerateFastPathFuncForEnumerable method:

private void GenerateFastPathFuncForEnumerable(SourceWriter writer, string serializeMethodName, TypeGenerationSpec typeGenerationSpec)

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-System.Text.JsonenhancementProduct code improvement that does NOT require public API changes/additions

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions