如何在不触发异常的情况下使用Json.net反序列化带有NodaTime.Instant的字典?

11

使用json.net序列化带有NodaTime.Instance的字典到json是没有问题的,但是在反序列化时会抛出Newtonsoft.Json.JsonSerializationException异常。

[Test] 
public void DeserializeDictionaryThowsException() {
    JsonConverter[] converters = { NodaConverters.IntervalConverter, NodaConverters.InstantConverter };

    var dictionary = new Dictionary<Instant, int>() {
         {Instant.FromUtc(2012, 1, 2, 3, 4, 5), 0}
    };            
    var json = JsonConvert.SerializeObject(dictionary, Formatting.None, converters);
    Assert.AreEqual("{\"2012-01-02T03:04:05Z\":0}", json); //ok
    var result = JsonConvert.DeserializeObject<Dictionary<Instant, int>>(json, converters); // throws
}

DeserializeObject抛出异常:

Newtonsoft.Json.JsonSerializationException:无法将字符串“2012-01-02T03:04:05Z”转换为字典键类型“NodaTime.Instant”。创建一个TypeConverter以将字符串转换为键类型对象。第1行,第24个位置。 ---->Newtonsoft.Json.JsonSerializationException:将值“2012-01-02T03:04:05Z”转换为类型“NodaTime.Instant”时出错。第1行,第24个位置。 ---->System.Exception:无法从System.String转换或转换为NodaTime.Instant。

顺带一提,反序列化DateTime的Dictionary正常工作。 我想这是因为String具有DateTime的转换器。

[Test]
public void DeserializeDiciotnaryOfDateTime() // OK
{
    var expected = new DateTime(2012, 1, 2, 3, 4, 5, DateTimeKind.Utc);
    var dictionary = new Dictionary<DateTime, int>() { { expected, 0 } };
    var json = JsonConvert.SerializeObject(dictionary);       
    var result = JsonConvert.DeserializeObject<Dictionary<DateTime, int>>(json); 
    Assert.AreEqual(expected, dictionary.Keys.First()); // OK
}

1
很抱歉之前没有看到这个问题。我对Json.NET了解不够,无法立即给您答复,但您能否在http://noda-time.googlecode.com上报告一个错误? - Jon Skeet
1
很抱歉之前我们没有注意到这个问题,感谢您的报告。我们正在此处跟踪此问题。 - Matt Johnson-Pint
2个回答

1

您需要添加更多的JSON.NET转换器,以将NodaTime.Instance时间序列化,如下所示。

public void DeserializeDictionaryThowsException()
{
    var dtzProvider = DateTimeZoneCache.GetSystemDefault();
    JsonConverter[] converters = { NodaConverters.IntervalConverter, 
                                   NodaConverters.InstantConverter,
                                   NodaConverters.LocalDateConverter,
                                   NodaConverters.LocalDateTimeConverter,
                                   NodaConverters.LocalTimeConverter,
                                   NodaConverters.OffsetConverter,
                                   NodaConverters.DurationConverter,
                                   NodaConverters.RoundtripPeriodConverter,
                                   NodaConverters.OffsetDateTimeConverter,
                                   NodaConverters.CreateDateTimeZoneConverter(dtzProvider),
                                   NodaConverters.CreateZonedDateTimeConverter(dtzProvider)
                                 };

    var dictionary = new Dictionary<Instant, int>() { { Instant.FromUtc(2012, 1, 2, 3, 4, 5), 0 } };
    var json = JsonConvert.SerializeObject(dictionary, Formatting.None, converters);
    Assert.AreEqual("{\"2012-01-02T03:04:05Z\":0}", json);
    var result = JsonConvert.DeserializeObject<Dictionary<Instant, int>>(json, converters);
}

var dtzProvider = DateTimeZoneCache.GetSystemDefault(); 给出的是:需要对象引用才能访问非静态字段、方法或属性“DateTimeZoneCache.GetSystemDefault()” - JP Hellemons
也许使用 var dtzProvider = DateTimeZoneProviders.Tzdb; 或 BCL? - JP Hellemons
这对我没用。我仍然会得到与帖子相同的异常。 - Steven

1
这个问题正在https://github.com/nodatime/nodatime.serialization/issues/2上解决。
我有一个修改自https://dev59.com/y2w15IYBdhLWcg3wFHpZ的解决方法。 这绝对不是高效的,也没有完全测试。
public class DictionaryWithNodaTimeKeyConverter : JsonConverter
{
    private static IDateTimeZoneProvider dtzProvider = DateTimeZoneProviders.Tzdb;
    private JsonSerializerSettings _settings;

    public DictionaryWithNodaTimeKeyConverter(IDateTimeZoneProvider dtzProvider)
        : base()
    {
        _settings = new JsonSerializerSettings().ConfigureForNodaTime(dtzProvider);
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        IDictionary dictionary = (IDictionary)value;
        writer.WriteStartObject();

        foreach (object key in dictionary.Keys)
        {
            writer.WritePropertyName(ConvertToPropertyKey(key));
            serializer.Serialize(writer, dictionary[key]);
        }

        writer.WriteEndObject();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
        {
            return null;
        }

        Type keyType = objectType.GetGenericArguments()[0];
        Type valueType = objectType.GetGenericArguments()[1];

        Type intermediateDictionaryType = typeof(Dictionary<,>).MakeGenericType(typeof(string), valueType);
        IDictionary intermediateDictionary = (IDictionary)Activator.CreateInstance(intermediateDictionaryType);
        serializer.Populate(reader, intermediateDictionary);

        IDictionary finalDictionary = (IDictionary)Activator.CreateInstance(objectType);
        foreach (DictionaryEntry pair in intermediateDictionary)
        {
            object parsedObject;
            if (TryConvertKey(pair.Key.ToString(), keyType, out parsedObject))
            {
                finalDictionary.Add(parsedObject, pair.Value);
            }
        }

        return finalDictionary;
    }

    public override bool CanConvert(Type objectType)
    {
        bool canConvert = objectType.IsA(typeof(IDictionary<,>));

        if (canConvert)
        {
            Type keyType = objectType.GetGenericArguments()[0];
            canConvert = canConvert && IsNodaTimeType(keyType);
        }

        return canConvert;
    }

    private bool IsNodaTimeType(Type type)
    {
        return type.IsA(typeof(Instant))
                || type.IsA(typeof(OffsetDateTime))
                || type.IsA(typeof(DateTimeZone))
                || type.IsA(typeof(ZonedDateTime))
                || type.IsA(typeof(LocalDateTime))
                || type.IsA(typeof(LocalDate))
                || type.IsA(typeof(LocalTime))
                || type.IsA(typeof(Offset))
                || type.IsA(typeof(Duration))
                || type.IsA(typeof(Period));
        // Interval is not Support because Interval is serialized as a compound object.
    }

    private string ConvertToPropertyKey(object property)
    {
        if (!IsNodaTimeType(property.GetType()))
        {
            throw new InvalidOperationException();
        }

        string result = JsonConvert.SerializeObject(property, _settings);
        if (!string.IsNullOrWhiteSpace(result))
        {
            // Remove the "" from JsonConvert
            int first = result.IndexOf('"');
            int last = result.LastIndexOf('"');
            if (first != -1 && last != -1 && first < last)
            {
                result = result.Substring(first + 1, last - (first + 1));
            }
        }

        return result;
    }

    private bool TryConvertKey(string text, Type keyType, out object value)
    {
        if (!IsNodaTimeType(keyType))
        {
            throw new InvalidOperationException();
        }

        value = keyType.CreateDefault();

        try
        {
            value = JsonConvert.DeserializeObject($"\"{text}\"", keyType, _settings);
            return true;
        }
        catch
        {
            return false;
        }
    }
}

我还定义了一些扩展

public static class TypeExtensions
{
    public static bool IsA(this Type type, Type typeToBe)
    {
        if (!typeToBe.IsGenericTypeDefinition)
            return typeToBe.IsAssignableFrom(type);

        List<Type> toCheckTypes = new List<Type> { type };
        if (typeToBe.IsInterface)
            toCheckTypes.AddRange(type.GetInterfaces());

        Type basedOn = type;
        while (basedOn.BaseType != null)
        {
            toCheckTypes.Add(basedOn.BaseType);
            basedOn = basedOn.BaseType;
        }

        return toCheckTypes.Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeToBe);
    }

    public static object CreateDefault(this Type type)
    {
        return type.IsValueType ? Activator.CreateInstance(type) : null;
    }
}

使用它:

IDateTimeZoneProvider provider = DateTimeZoneProviders.Tzdb;
JsonConverter[] converters = { NodaConverters.IntervalConverter, NodaConverters.InstantConverter, new DictionaryWithNodaTimeKeyConverter(provider) };

var dictionary = new Dictionary<Instant, int> {
    { Instant.FromUtc(2012, 1, 2, 3, 4, 5), 0 }
};

var json = JsonConvert.SerializeObject(dictionary, Formatting.None, converters);
Console.WriteLine(json);
var result = JsonConvert.DeserializeObject<Dictionary<Instant, int>>(json, converters);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接