Home > OS >  How to Deserialize JSON to Dictionary with non-string (e.g. FileInfo) key type
How to Deserialize JSON to Dictionary with non-string (e.g. FileInfo) key type

Time:01-08

I have this rather simple test and trying to get it working for days now, without success:

using System.Drawing;
using Newtonsoft.Json;

namespace JSON_Trials;

class StackExample {
    public static void Main() {
        Dictionary<FileInfo, Point> dict_write = new() {
            {new("a_file_name.txt"), new(1, 2)},
        };

        var json_write = JsonConvert.SerializeObject(dict_write);
        File.WriteAllText("_dict.json", json_write);

        var json_read = File.ReadAllText("_dict.json");
        var dict_read = JsonConvert.DeserializeObject<Dictionary<FileInfo, Point>>(json_read);
        Console.WriteLine(dict_read.Count);
    }
}

Basically, I'm just trying to round trip the Dictionary.

In this basic form, it creates the following error:

Newtonsoft.Json.JsonSerializationException: 'Could not convert string 'a_file_name.txt' to dictionary key type 'System.IO.FileInfo'. Create a TypeConverter to convert from the string to the key type object. Path '['a_file_name.txt']', line 1, position 19.'

Just for completeness, this is the generated json in the file:

{"a_file_name.txt":"1, 2"}

I have written many TypeConverters, JsonConverters, ContractResolverthingys and such and none of these approaches work. What am I missing here? Is there a way to do this at all? It should be super easy, right? I mean FileInfo has a single string constructor and the json is in the format of a Dictionary anyways. Any hint is much appreciated even if the "solution" might not be straight forward.

Since there are 3 (three!) valid solutions now, some clarification of this particular scenario seems appropriate:

  • The Key of the Dictionary is a type of object, that has a 'single string' constructor (in this case, FileInfo(string filename)).
  • The type of the Key is not controlled (source code can not be changed, no annotations added).
  • The JSON should stay the same (no arrays).
  • The Value doesn't really matter here.

P.S.: For all versions, we can just assume the most recent one. atm: .NET 7 & C# 11

Solution

This is the final code I ended up with, thanks @Serg! I also use FullName, as the full name is close enough to a unique identifier for me :-)

using System.ComponentModel;
using System.Drawing;
using System.Globalization;
using Newtonsoft.Json;

namespace JSON_Trials;

class StackExample {
    public static void Main() {
        Dictionary<FileInfo, Point> dict_write = new() {
            {new("a_file_name.txt"), new(1, 2)},
        };

        TypeDescriptor.AddAttributes(typeof(FileInfo), new TypeConverterAttribute(typeof(FileInfoTypeConverter)));

        var json_write = JsonConvert.SerializeObject(dict_write);
        File.WriteAllText("_dict.json", json_write);

        var json_read = File.ReadAllText("_dict.json");
        var dict_read = JsonConvert.DeserializeObject<Dictionary<FileInfo, Point>>(json_read);
        Console.WriteLine(dict_read.Count);
    }
}

internal class FileInfoTypeConverter : TypeConverter {
    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) {
        if (sourceType == typeof(string))
            return true;
        return base.CanConvertFrom(context, sourceType);
    }

    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) {
        if (value is string str)
            return new FileInfo(str);
        return base.ConvertFrom(context, culture, value);
    }

    public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) {
        if (value is FileInfo fi)
            return fi.FullName;
        return base.ConvertTo(context, culture, value, destinationType);
    }
}

CodePudding user response:

There are a several causes of such a behaviour.

  1. In the JSON the dictionaries may have only strings as a keys. No objects allowed there. So, when you serializing your dictionary, you see the result of FileInfo.ToString as a key. Normally, outside of dictionary, the FileInfo object is not serializable at all and JsonConvert.SerializeObject(new FileInfo("test.txt")); will throw an exception. This may be solved with custom ContractResolver which will force to process dictionary as an array which elements are key-value pairs from a dictionary. More details https://stackoverflow.com/a/25064637/2011071
  2. FileInfo is not serializable out-of-the-box, so you need to provide appropriate JsonConverter for this.

Update: if a dictionary-like JSON format is mandatory, I only know the two-stage solution, see at the end of the post

So, the final (simplified) solution may looks like this:

using System.Collections;
using System.Drawing;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;

namespace JSON_Trials;

class StackExample
{
    public static void Main()
    {
        Dictionary<FileInfo, Point> dict_write = new() 
        {
            {new("a_file_name.txt"), new(1, 2)},
        };

        var jsonSerializerSettings = new JsonSerializerSettings
        {
            ContractResolver = new DictionaryAsArrayResolver(),
            Converters = new List<JsonConverter>
            {
                new FileInfoJsonConverter(),
            }
        };

        var json_write = JsonConvert.SerializeObject(dict_write, jsonSerializerSettings);
        File.WriteAllText("_dict.json", json_write);

        var json_read = File.ReadAllText("_dict.json");
        var dict_read = JsonConvert.DeserializeObject<Dictionary<FileInfo, Point>>(json_read, jsonSerializerSettings);
        Console.WriteLine(dict_read.Count);
    }
}

internal class DictionaryAsArrayResolver : DefaultContractResolver
{
    protected override JsonContract CreateContract(Type objectType)
    {
        if (objectType
            .GetInterfaces()
            .Any(i => i == typeof(IDictionary)
                      || (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IDictionary<,>))))
        {
            return base.CreateArrayContract(objectType);
        }

        return base.CreateContract(objectType);
    }
}

public class FileInfoJsonConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var obj = JToken.FromObject(value.ToString());
        obj.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType != JsonToken.String)
        {
            throw new Exception("Can only read from strings.");
        }
        return new FileInfo((string)reader.Value);
    }

    public override bool CanRead => true;

    public override bool CanConvert(Type objectType) => objectType == typeof(FileInfo);
}

Update: To have JSON in dictionary-style format, the only way I know will be to deserialize into the Dictionary<string, Point first and then convert string key into the FileInfo

var tmp = JsonConvert.DeserializeObject<Dictionary<string, Point>>(json_read);
var dict_read = tmp.ToDictionary(kv => new FileInfo(kv.Key), kv => kv.Value);

In this sample you do not need any additional ContractResolver or JsonConverter at all.

Update 2: the other solution is to assign a TypeConverter to the FileInfo, but it can be done only globally so I can't estimate the possible side effects and can't r recommend is way in a production.

//call once somewhere before deserialization
TypeDescriptor.AddAttributes(typeof(FileInfo), new TypeConverterAttribute(typeof(FileInfoTypeConverter)));


internal class FileInfoTypeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
    {
        if (sourceType == typeof(string))
        {
            return true;
        }

        return base.CanConvertFrom(context, sourceType);
    }

    public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
    {
        if (value is string str)
        {
            return new FileInfo(str);
        }
        return base.ConvertFrom(context, culture, value);
    }

    public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
    {
        if (value is FileInfo fi)
        {
            return fi.Name;
        }
        return base.ConvertTo(context, culture, value, destinationType);
    }
}
  • Related