From 7f7cccd598038f9e51d8b121f04373702cd1d7eb Mon Sep 17 00:00:00 2001 From: JamesNK Date: Tue, 23 Mar 2010 00:21:57 +0000 Subject: [PATCH] -Add support for using implicit/explicit casts when converting a JSON property name to a dictionary value -Fix DynamicReflectionDelegateFactory erroring when calling static methods -Fix CustomCreationConverter to return null when converting a null JSON value --- .../CustomCreationConverterTests.cs | 81 +++++++++++++++++++ .../Serialization/JsonSerializerTest.cs | 38 ++++++++- .../DynamicReflectionDelegateFactoryTests.cs | 18 +++++ .../Converters/CustomCreationConverter.cs | 3 + .../JsonSerializerInternalReader.cs | 2 +- Src/Newtonsoft.Json/Utilities/ConvertUtils.cs | 78 ++++++++++++++++-- .../DynamicReflectionDelegateFactory.cs | 2 +- 7 files changed, 214 insertions(+), 8 deletions(-) diff --git a/Src/Newtonsoft.Json.Tests/Converters/CustomCreationConverterTests.cs b/Src/Newtonsoft.Json.Tests/Converters/CustomCreationConverterTests.cs index f50f085a..43a15023 100644 --- a/Src/Newtonsoft.Json.Tests/Converters/CustomCreationConverterTests.cs +++ b/Src/Newtonsoft.Json.Tests/Converters/CustomCreationConverterTests.cs @@ -145,5 +145,86 @@ namespace Newtonsoft.Json.Tests.Converters const string expected = @"{""Value"":""Foo"",""Thing"":{""Number"":456}}"; Assert.AreEqual(expected, json); } + + internal interface IRange + { + T First { get; } + T Last { get; } + } + + internal class Range : IRange + { + public T First { get; set; } + public T Last { get; set; } + } + + internal class NullInterfaceTestClass + { + public virtual Guid Id { get; set; } + public virtual int? Year { get; set; } + public virtual string Company { get; set; } + public virtual IRange DecimalRange { get; set; } + public virtual IRange IntRange { get; set; } + public virtual IRange NullDecimalRange { get; set; } + } + + internal class DecimalRangeConverter : CustomCreationConverter> + { + public override IRange Create(Type objectType) + { + return new Range(); + } + } + + internal class IntRangeConverter : CustomCreationConverter> + { + public override IRange Create(Type objectType) + { + return new Range(); + } + } + + [Test] + public void DeserializeAndConvertNullValue() + { + NullInterfaceTestClass initial = new NullInterfaceTestClass + { + Company = "Company!", + DecimalRange = new Range { First = 0, Last = 1 }, + Id = new Guid(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), + IntRange = new Range { First = int.MinValue, Last = int.MaxValue }, + Year = 2010, + NullDecimalRange = null + }; + + string json = JsonConvert.SerializeObject(initial, Formatting.Indented); + + Assert.AreEqual(@"{ + ""Id"": ""00000001-0002-0003-0405-060708090a0b"", + ""Year"": 2010, + ""Company"": ""Company!"", + ""DecimalRange"": { + ""First"": 0.0, + ""Last"": 1.0 + }, + ""IntRange"": { + ""First"": -2147483648, + ""Last"": 2147483647 + }, + ""NullDecimalRange"": null +}", json); + + NullInterfaceTestClass deserialized = JsonConvert.DeserializeObject( + json, new IntRangeConverter(), new DecimalRangeConverter()); + + Assert.AreEqual("Company!", deserialized.Company); + Assert.AreEqual(new Guid(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), deserialized.Id); + Assert.AreEqual(0, deserialized.DecimalRange.First); + Assert.AreEqual(1, deserialized.DecimalRange.Last); + Assert.AreEqual(int.MinValue, deserialized.IntRange.First); + Assert.AreEqual(int.MaxValue, deserialized.IntRange.Last); + Assert.AreEqual(null, deserialized.NullDecimalRange); + Assert.AreEqual(2010, deserialized.Year); + } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs index a2362ab7..0ace8424 100644 --- a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs +++ b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs @@ -2000,7 +2000,7 @@ keyword such as type of business."" } [Test] - [ExpectedException(typeof(JsonSerializationException), ExpectedMessage = "Could not convert string 'Newtonsoft.Json.Tests.TestObjects.Person' to dictionary key type Newtonsoft.Json.Tests.TestObjects.Person. Create a TypeConverter to convert from the string to the key type object.")] + [ExpectedException(typeof(JsonSerializationException), ExpectedMessage = "Could not convert string 'Newtonsoft.Json.Tests.TestObjects.Person' to dictionary key type 'Newtonsoft.Json.Tests.TestObjects.Person'. Create a TypeConverter to convert from the string to the key type object.")] public void DeserializePersonKeyedDictionary() { string json = @@ -3316,5 +3316,41 @@ keyword such as type of business."" Assert.AreEqual("James", deserialized.Name); Assert.AreEqual(27, deserialized.Age); } + + public class DictionaryKey + { + public string Value { get; set; } + + public override string ToString() + { + return Value; + } + + public static implicit operator DictionaryKey(string value) + { + return new DictionaryKey() {Value = value}; + } + } + + [Test] + public void SerializeDeserializeDictionaryKey() + { + Dictionary dictionary = new Dictionary(); + + dictionary.Add(new DictionaryKey() { Value = "First!" }, "First"); + dictionary.Add(new DictionaryKey() { Value = "Second!" }, "Second"); + + string json = JsonConvert.SerializeObject(dictionary, Formatting.Indented); + + Assert.AreEqual(@"{ + ""First!"": ""First"", + ""Second!"": ""Second"" +}", json); + + Dictionary newDictionary = + JsonConvert.DeserializeObject>(json); + + Assert.AreEqual(2, newDictionary.Count); + } } } \ No newline at end of file diff --git a/Src/Newtonsoft.Json.Tests/Utilities/DynamicReflectionDelegateFactoryTests.cs b/Src/Newtonsoft.Json.Tests/Utilities/DynamicReflectionDelegateFactoryTests.cs index bbe80e0c..c8123fe9 100644 --- a/Src/Newtonsoft.Json.Tests/Utilities/DynamicReflectionDelegateFactoryTests.cs +++ b/Src/Newtonsoft.Json.Tests/Utilities/DynamicReflectionDelegateFactoryTests.cs @@ -27,10 +27,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using System.Text; using NUnit.Framework; using Newtonsoft.Json.Utilities; using Newtonsoft.Json.Tests.TestObjects; +using Newtonsoft.Json.Tests.Serialization; namespace Newtonsoft.Json.Tests.Utilities { @@ -89,6 +91,22 @@ namespace Newtonsoft.Json.Tests.Utilities setter(m, new Version()); } + + [Test] + public void CreateStaticMethodCall() + { + MethodInfo castMethodInfo = typeof(JsonSerializerTest.DictionaryKey).GetMethod("op_Implicit", new[] { typeof(string) }); + + Assert.IsNotNull(castMethodInfo); + + MethodCall call = DynamicReflectionDelegateFactory.Instance.CreateMethodCall(castMethodInfo); + + object result = call(null, "First!"); + Assert.IsNotNull(result); + + JsonSerializerTest.DictionaryKey key = (JsonSerializerTest.DictionaryKey) result; + Assert.AreEqual("First!", key.Value); + } } } #endif \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Converters/CustomCreationConverter.cs b/Src/Newtonsoft.Json/Converters/CustomCreationConverter.cs index ecd8e618..a7eb4996 100644 --- a/Src/Newtonsoft.Json/Converters/CustomCreationConverter.cs +++ b/Src/Newtonsoft.Json/Converters/CustomCreationConverter.cs @@ -54,6 +54,9 @@ namespace Newtonsoft.Json.Converters /// The object value. public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { + if (reader.TokenType == JsonToken.Null) + return null; + T value = Create(objectType); if (value == null) throw new JsonSerializationException("No object created."); diff --git a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs index 1228bcce..3dfb147c 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs @@ -573,7 +573,7 @@ namespace Newtonsoft.Json.Serialization } catch (Exception ex) { - throw new JsonSerializationException("Could not convert string '{0}' to dictionary key type {1}. Create a TypeConverter to convert from the string to the key type object.".FormatWith(CultureInfo.InvariantCulture, reader.Value, contract.DictionaryKeyType), ex); + throw new JsonSerializationException("Could not convert string '{0}' to dictionary key type '{1}'. Create a TypeConverter to convert from the string to the key type object.".FormatWith(CultureInfo.InvariantCulture, reader.Value, contract.DictionaryKeyType), ex); } CheckedRead(reader); diff --git a/Src/Newtonsoft.Json/Utilities/ConvertUtils.cs b/Src/Newtonsoft.Json/Utilities/ConvertUtils.cs index a50b4faf..4c9ea1de 100644 --- a/Src/Newtonsoft.Json/Utilities/ConvertUtils.cs +++ b/Src/Newtonsoft.Json/Utilities/ConvertUtils.cs @@ -30,6 +30,7 @@ using System.Text; using System.Globalization; using System.ComponentModel; using Newtonsoft.Json.Serialization; +using System.Reflection; #if !SILVERLIGHT using System.Data.SqlTypes; @@ -39,6 +40,63 @@ namespace Newtonsoft.Json.Utilities { internal static class ConvertUtils { + internal struct TypeConvertKey : IEquatable + { + private readonly Type _initialType; + private readonly Type _targetType; + + public Type InitialType + { + get { return _initialType; } + } + + public Type TargetType + { + get { return _targetType; } + } + + public TypeConvertKey(Type initialType, Type targetType) + { + _initialType = initialType; + _targetType = targetType; + } + + public override int GetHashCode() + { + return _initialType.GetHashCode() ^ _targetType.GetHashCode(); + } + + public override bool Equals(object obj) + { + if (!(obj is TypeConvertKey)) + return false; + + return Equals((TypeConvertKey)obj); + } + + public bool Equals(TypeConvertKey other) + { + return (_initialType == other._initialType && _targetType == other._targetType); + } + } + + private static readonly ThreadSafeStore> CastConverters = + new ThreadSafeStore>(CreateCastConverter); + + private static Func CreateCastConverter(TypeConvertKey t) + { + MethodInfo castMethodInfo = t.TargetType.GetMethod("op_Implicit", new[] { t.InitialType }); + if (castMethodInfo == null) + castMethodInfo = t.TargetType.GetMethod("op_Explicit", new[] { t.InitialType }); + + if (castMethodInfo == null) + return null; + + MethodCall call = JsonTypeReflector.ReflectionDelegateFactory.CreateMethodCall(castMethodInfo); + + return o => call(null, o); + } + public static bool CanConvertType(Type initialType, Type targetType, bool allowTypeNameToString) { ValidationUtils.ArgumentNotNull(initialType, "initialType"); @@ -387,12 +445,22 @@ namespace Newtonsoft.Json.Utilities { Type valueType = (value != null) ? value.GetType() : null; - if (value != null && targetType.IsAssignableFrom(valueType)) - return value; - else if (value == null && ReflectionUtils.IsNullable(targetType)) - return null; + if (value != null) + { + if (targetType.IsAssignableFrom(valueType)) + return value; + + Func castConverter = CastConverters.Get(new TypeConvertKey(valueType, targetType)); + if (castConverter != null) + return castConverter(value); + } else - throw new Exception("Could not cast or convert from {0} to {1}.".FormatWith(CultureInfo.InvariantCulture, (initialType != null) ? initialType.ToString() : "{null}", targetType)); + { + if (ReflectionUtils.IsNullable(targetType)) + return null; + } + + throw new Exception("Could not cast or convert from {0} to {1}.".FormatWith(CultureInfo.InvariantCulture, (initialType != null) ? initialType.ToString() : "{null}", targetType)); } #if !SILVERLIGHT diff --git a/Src/Newtonsoft.Json/Utilities/DynamicReflectionDelegateFactory.cs b/Src/Newtonsoft.Json/Utilities/DynamicReflectionDelegateFactory.cs index 3bffe953..e1e6b919 100644 --- a/Src/Newtonsoft.Json/Utilities/DynamicReflectionDelegateFactory.cs +++ b/Src/Newtonsoft.Json/Utilities/DynamicReflectionDelegateFactory.cs @@ -66,7 +66,7 @@ namespace Newtonsoft.Json.Utilities generator.MarkLabel(argsOk); - if (!method.IsConstructor) + if (!method.IsConstructor && !method.IsStatic) generator.PushInstance(method.DeclaringType); for (int i = 0; i < args.Length; i++)