From b5f1b8319133ab63dcd155f12922c37a8c9b7185 Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 26 Feb 2017 19:11:45 +1300 Subject: [PATCH] -Added improved support for deserializing immutable structs --- .../Serialization/JsonSerializerTest.cs | 42 +++++++++++ .../Serialization/DefaultContractResolver.cs | 69 ++++++++++++++++--- 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs index 839d20c0..e56b9f44 100644 --- a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs +++ b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs @@ -96,6 +96,48 @@ namespace Newtonsoft.Json.Tests.Serialization [TestFixture] public class JsonSerializerTest : TestFixtureBase { + public struct ImmutableStruct + { + public ImmutableStruct(string value) + { + Value = value; + Value2 = 0; + } + + public string Value { get; } + public int Value2 { get; set; } + } + + [Test] + public void DeserializeImmutableStruct() + { + var result = JsonConvert.DeserializeObject("{ \"Value\": \"working\", \"Value2\": 2 }"); + + Assert.AreEqual("working", result.Value); + Assert.AreEqual(2, result.Value2); + } + + public struct AlmostImmutableStruct + { + public AlmostImmutableStruct(string value, int value2) + { + Value = value; + Value2 = value2; + } + + public string Value { get; } + public int Value2 { get; set; } + } + + [Test] + public void DeserializeAlmostImmutableStruct() + { + var result = JsonConvert.DeserializeObject("{ \"Value\": \"working\", \"Value2\": 2 }"); + + Assert.AreEqual(null, result.Value); + Assert.AreEqual(2, result.Value2); + } + public class ErroringClass { public DateTime Tags { get; set; } diff --git a/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs b/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs index 58a7ed3a..c2b978ac 100644 --- a/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs +++ b/Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs @@ -373,6 +373,17 @@ namespace Newtonsoft.Json.Serialization contract.CreatorParameters.AddRange(CreateConstructorParameters(constructor, contract.Properties)); } } + else if (contract.NonNullableUnderlyingType.IsValueType()) + { + // value types always have default constructor + // check whether there is a constructor that matches with non-writable properties on value type + ConstructorInfo constructor = GetImmutableConstructor(contract.NonNullableUnderlyingType, contract.Properties); + if (constructor != null) + { + contract.OverrideCreator = JsonTypeReflector.ReflectionDelegateFactory.CreateParameterizedConstructor(constructor); + contract.CreatorParameters.AddRange(CreateConstructorParameters(constructor, contract.Properties)); + } + } } MemberInfo extensionDataMember = GetExtensionDataMemberForType(contract.NonNullableUnderlyingType); @@ -566,6 +577,35 @@ namespace Newtonsoft.Json.Serialization return null; } + private ConstructorInfo GetImmutableConstructor(Type objectType, JsonPropertyCollection memberProperties) + { + IEnumerable constructors = objectType.GetConstructors(); + IEnumerator en = constructors.GetEnumerator(); + if (en.MoveNext()) + { + ConstructorInfo constructor = en.Current; + if (!en.MoveNext()) + { + ParameterInfo[] parameters = constructor.GetParameters(); + if (parameters.Length > 0) + { + foreach (ParameterInfo parameterInfo in parameters) + { + var memberProperty = MatchProperty(memberProperties, parameterInfo.Name, parameterInfo.ParameterType); + if (memberProperty == null || memberProperty.Writable) + { + return null; + } + } + + return constructor; + } + } + } + + return null; + } + private ConstructorInfo GetParameterizedConstructor(Type objectType) { #if PORTABLE @@ -603,15 +643,7 @@ namespace Newtonsoft.Json.Serialization foreach (ParameterInfo parameterInfo in constructorParameters) { - // it is possible to generate a ParameterInfo with a null name using Reflection.Emit - // protect against an ArgumentNullException from GetClosestMatchProperty by testing for null here - JsonProperty matchingMemberProperty = (parameterInfo.Name != null) ? memberProperties.GetClosestMatchProperty(parameterInfo.Name) : null; - - // type must match as well as name - if (matchingMemberProperty != null && matchingMemberProperty.PropertyType != parameterInfo.ParameterType) - { - matchingMemberProperty = null; - } + JsonProperty matchingMemberProperty = MatchProperty(memberProperties, parameterInfo.Name, parameterInfo.ParameterType); // ensure that property will have a name from matching property or from parameterinfo // parameterinfo could have no name if generated by a proxy (I'm looking at you Castle) @@ -629,6 +661,25 @@ namespace Newtonsoft.Json.Serialization return parameterCollection; } + private JsonProperty MatchProperty(JsonPropertyCollection properties, string name, Type type) + { + // it is possible to generate a member with a null name using Reflection.Emit + // protect against an ArgumentNullException from GetClosestMatchProperty by testing for null here + if (name == null) + { + return null; + } + + JsonProperty property = properties.GetClosestMatchProperty(name); + // must match type as well as name + if (property == null || property.PropertyType != type) + { + return null; + } + + return property; + } + /// /// Creates a for the given . ///