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.
- 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, theFileInfo
object is not serializable at all andJsonConvert.SerializeObject(new FileInfo("test.txt"));
will throw an exception. This may be solved with customContractResolver
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 FileInfo
is not serializable out-of-the-box, so you need to provide appropriateJsonConverter
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);
}
}