2

I am attempting to use JSON.NET to serialize/deserialize a SortedDictionary<Time, T>, where Time is a custom class in my project, and T is some type that need not be included here.

Serialization works perfectly. Take this dictionary, for example:

var dictionary = new SortedDictionary<Time, T> {
  [Time.Parse("7:00AM")] = ...,
  [Time.Parse("8:00AM")] = ...,
  [Time.Parse("9:00AM")] = ...
};

(Time.Parse() is a simple method that constructs Time objects from a string input.) Now, if I run JsonConvert.SerializeObject(dictionary, Formatting.Indented), I get something like this:

{
  "7:00AM": ...,
  "8:00AM": ...,
  "9:00AM": ...
}

This is because the Time class overrides the ToString() method, so the dictionary keys can easily be converted to strings.

Unfortunately, deserialization fails here. Given the output of the last example, running JsonConvert.DeserializeObject<SortedDictionary<Time, T>>(previousOutput) produces an exception:

Newtonsoft.Json.JsonSerializationException

Could not convert string '7:00AM' to dictionary key type '[...].Time'. Create a TypeConverter to convert from the string to the key type object. [...]

Okay, that makes sense. After all, without some kind of converter, there is no way for JSON.NET to know how to handle this. However, my Time class already has a Parse(string) method. So, I created a TypeConverter called TimeConverter that utilizes it, like so:

public class TimeConverter : TypeConverter
{
    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        return sourceType == typeof(string) && base.CanConvertFrom(context, sourceType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        return value is string s ? Time.Parse(s) : base.ConvertFrom(context, culture, value);
    }

    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
        return destinationType == typeof(string) && value is Time t
        ? t.ToString()
        : base.ConvertTo(context, culture, value, destinationType);
    }
}

I then added a TypeConverter attribute to the Time class declaration:

[TypeConverter(typeof(TimeConverter))]
public class Time : IComparable<Time>
{
   ...
}

However, this did not work. The exact same error is still thrown. Additionally, I tried placing breakpoints in the TimeConverter methods, but they were never triggered. JSON.NET seems to be completely ignoring my converter. What am I doing wrong?

dbc
  • 104,963
  • 20
  • 228
  • 340
Jacob Lockard
  • 1,195
  • 9
  • 24

2 Answers2

3

You need to modify TimeConverter.CanConvertFrom() to return true if the incoming type is typeof(string) or the base class can convert the type:

public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
    return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
}

As can be seen from the reference source, the base class's CanConvertFrom() only returns true when the incoming type is typeof(InstanceDescriptor). Thus your CanConvertFrom() return false when passed typeof(string) and the conversion methods never got called.

A demo of the original problem using a mockup for Time class can be found here.

A demo of the fixed, working TypeConverter can be found here.

Note however that, if you are using .NET Core, support for type converters was only added as of Json.NET 10.0.1. If you are targeting netstandard1.0+ or .NET Core and are using an earlier version Json.NET you will need to upgrade. See Json.Net: Serialize/Deserialize property as a value, not as an object for details.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • I see what you mean, and I've changed my code, but for some reason it still doesn't work. `CanConvertFrom()` never even seems to be called. However, your fiddle does seem to work, so I'm not sure what I'm doing differently. Any suggestions? – Jacob Lockard Dec 26 '20 at 21:59
  • @JacobLockard - Then please [edit] your question to include a [mcve]. I had to create a mockup for `Time` and make a guess as to the `T` type in the dictionary. You're more likely to get useful debugging help if we can reproduce your problem. See: [ask]. I initially had to create my own [MCVE] for your problem here: https://dotnetfiddle.net/NicoO1 – dbc Dec 26 '20 at 22:02
  • @JacobLockard - Does your actual `Time` class also implement `IConvertible`? If so, maybe see [Newtonsoft.Json - DeserializeObject throws when deserializing custom type: Error converting value “somestring” to type CustomType](https://stackoverflow.com/q/64972694/3744182). – dbc Dec 26 '20 at 22:09
  • Well, after an example on DotNetFiddle worked, I finally decided to just try updating my version of Newtonsoft.Json. Sure enough, upgrading from 9.* to 12.* solved the issue! Thanks for your help! What is the recommended action in this case? Should I create a new answer with what I did and mark it as the accepted one, or should I just leave the question be? – Jacob Lockard Dec 26 '20 at 22:25
  • @JacobLockard - Even if you update from Json.NET 9 to 12, I don't think your converter will work unless you make `CanConvertFrom()` return true. My failing fiddle https://dotnetfiddle.net/NicoO1 uses Json.NET 12. But it's quite possible you would need to do both if you are working in .Net Core, as Json.NET did not support type converters **in .Net Core** until [release 10](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/10.0.1). – dbc Dec 26 '20 at 22:35
  • Yes, sorry, that's what I meant. After implementing your changes *and* updating it works. Thus my confusion: your answer helped but didn't exactly solve the problem (and thus, is not exactly the "Accepted Answer"). How should I handle this? Oh, and I was using .NetStandard 2.1. – Jacob Lockard Dec 26 '20 at 22:43
1

It turns out that this was a combination of two different issues:

  1. As explained in @dbc's answer, I was using && instead of || in TimeConverter.CanConvertFrom().
  2. I was using version 9 of Json.NET, but in .NET Core, TypeConverter support was not implemented until version 10.

Using || and upgrading to Json.NET 12 solved my issue.

Jacob Lockard
  • 1,195
  • 9
  • 24