I am trying to read a json file using a c# option pattern. However, I am running into a problem where a property of Dictionary<int, myClass> is not being mapped correctly unless I changed it the key to string like Dictionary<string, myClass>
This is my example json file: test.json
{
"TestMap": [
{
"SenderID": "RIMC_EVAC_ZONES",
"FeedType": null,
"CategoryMappings": {
"4": {
"CategoryID": 4,
"VccCategoryName": "Local Disaster",
"ReiCategoryName": "Local Disaster",
"SubcategoryMappings": {
"177": [
{
"SubcategoryID": 177,
"ParentCategoryID": 4,
"VccSubcategoryName": "Evacuation",
"ReiSubcategoryName": "Evacuation"
}
]
}
}
}
},
{
"SenderID": "EARLY_HURRICANE",
"FeedType": null,
"CategoryMappings": {
"16": {
"CategoryID": 16,
"VccCategoryName": "Tropical Storm",
"ReiCategoryName": "Tropical Storm",
"SubcategoryMappings": null
}
}
}
]
}
This is my model:
public class Sender {
public string SenderID { get; set; }
public string FeedType { get; set; }
//[JsonConverter(typeof(IntKeyDictionaryConverter))]
public Dictionary<int, CategoryMapping> CategoryMappings { get; set; }
}
public class CategoryMapping {
public int CategoryID { get; set; }
public string VccCategoryName { get; set; }
public string ReiCategoryName { get; set; }
public Dictionary<int, List<SubcategoryMapping>> SubcategoryMappings { get; set; }
}
public class SubcategoryMapping
{
public int SubcategoryID { get; set; }
public int ParentCategoryID { get; set; }
public string VccSubcategoryName { get; set; }
public string ReiSubcategoryName { get; set; }
}
How I register the page:
builder.Configuration.AddJsonFile("Properties/test.json", optional: true, reloadOnChange: true)
And How I called to retrive the data:
List<Sender> people = Configuration.GetSection("TestMap").Get<List<Sender>>();
Problem:: with my current model, The variable "People" will have its CategoryMapping as null but will retrieve other information such as SenderID or FeedType correctly.
However, if I change the "CategoryingMappings" to Dictionary<string, CategoryMapping>, then it will be mapped correctly. Same thing happened to "SubcategoryMapping" property of CategoryMapping class.
What I have tried: I tried to write custom converter, but it didn't work and cause the conversion to fail
public class IntKeyDictionaryConverter : JsonConverter<Dictionary<int, CategoryMapping>>
{
public override Dictionary<int, CategoryMapping> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var dictionary = new Dictionary<int, CategoryMapping>();
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return dictionary;
}
if (reader.TokenType == JsonTokenType.PropertyName)
{
int key = int.Parse(reader.GetString());
reader.Read();
CategoryMapping value = JsonSerializer.Deserialize<CategoryMapping>(ref reader, options);
dictionary.Add(key, value);
}
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, Dictionary<int, CategoryMapping> value, JsonSerializerOptions options) {
}
}
What I need help with : I want integer as key when read the json into my class object. How do I achieve that?
Your problem is that
Microsoft.Extensions.Configuration.Binderdoes not use System.Text.Json to deserialize JSON. Instead it manually parses the JSON toIConfigurationSectionelements then binds those to POCOs using reflection. None of this seems to be documented, so for confirmation you may check the source code forConfigurationBinder.In the .NET 6, the method
BindDictionary()is used to bind to a dictionary. It has a commented restriction on dictionary key types tostringand enum types:In .NET 7 an additional method
BindDictionaryInterface()exists which contains logic (added via PR #71609) that explicitly supports numeric keys:This explains why the
public Dictionary<int, CategoryMapping> CategoryMappings { get; set; }property can be bound successfully only after moving to .NET 7.If you cannot move to .NET 7 but still require the use of dictionaries with integer keys, then as a workaround you could use the adapter pattern to wrap your integer dictionaries in
IDictionary<string, TValue>surrogates.First, define the following
IntegerDictionaryAdapter<TValue>class:Then modify your classes to use it as follows:
And now you will be able to bind your configuration data model in .NET 6 (and .NET 7 as well) as follows, using the
UnderlyingDictionaryproperty to access your integer-keyed dictionaries:(Honestly however I'm not sure it's worth the trouble.)
Demo fiddle here.