From 45d5e90da1e29ab34ec121ad06a4145b2bb9721b Mon Sep 17 00:00:00 2001 From: Inverness Date: Sat, 8 Nov 2014 19:10:12 -0600 Subject: [PATCH 1/2] Support reference preservation for ISerializable objects. --- .../Serialization/JsonSerializerTest.cs | 106 ++++++++++++++++++ .../Serialization/JsonFormatterConverter.cs | 15 ++- .../JsonISerializableContract.cs | 8 ++ .../JsonSerializerInternalReader.cs | 52 ++++----- .../JsonSerializerInternalWriter.cs | 7 +- 5 files changed, 157 insertions(+), 31 deletions(-) diff --git a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs index 8da564ea..afcc9cff 100644 --- a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs +++ b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs @@ -699,6 +699,112 @@ namespace Newtonsoft.Json.Tests.Serialization Assert.AreEqual(ratio.Denominator, ratio2.Denominator); Assert.AreEqual(ratio.Numerator, ratio2.Numerator); } + + public class IDeserializationCallbackTestObject : ISerializable, IDeserializationCallback + { + internal string _stringValue; + internal int _intValue; + internal PersonReference _person1; + internal PersonReference _person2; + internal PersonReference _person3; + internal SerializationInfo _serializationInfo; + + public IDeserializationCallbackTestObject(string stringValue, int intValue, PersonReference p1, PersonReference p2, PersonReference p3) + { + _stringValue = stringValue; + _intValue = intValue; + _person1 = p1; + _person2 = p2; + _person3 = p3; + } + + protected IDeserializationCallbackTestObject(SerializationInfo info, StreamingContext context) + { + _serializationInfo = info; + } + + public void GetObjectData(SerializationInfo info, StreamingContext context) + { + info.AddValue("stringValue", _stringValue); + info.AddValue("intValue", _intValue); + info.AddValue("person1", _person1, typeof(PersonReference)); + info.AddValue("person2", _person2, typeof(PersonReference)); + info.AddValue("person3", _person3, typeof(PersonReference)); + } + + void IDeserializationCallback.OnDeserialization(object sender) + { + if (_serializationInfo == null) + return; + + _stringValue = _serializationInfo.GetString("stringValue"); + _intValue = _serializationInfo.GetInt32("intValue"); + _person1 = (PersonReference) _serializationInfo.GetValue("person1", typeof(PersonReference)); + _person2 = (PersonReference) _serializationInfo.GetValue("person2", typeof(PersonReference)); + _person3 = (PersonReference) _serializationInfo.GetValue("person3", typeof(PersonReference)); + + _serializationInfo = null; + } + } + + [Test] + public void DeserializeIDeserializationCallback() + { + var p1 = new PersonReference + { + Name = "John Smith" + }; + var p2 = new PersonReference + { + Name = "Mary Sue", + }; + + p1.Spouse = p2; + p2.Spouse = p1; + + var obj = new IDeserializationCallbackTestObject("string!", 42, p1, p2, p1); + + var settings = new JsonSerializerSettings + { + PreserveReferencesHandling = PreserveReferencesHandling.All, + Formatting = Formatting.Indented + }; + + string json = JsonConvert.SerializeObject(obj, settings); + + StringAssert.AreEqual(json, @"{ + ""$id"": ""1"", + ""stringValue"": ""string!"", + ""intValue"": 42, + ""person1"": { + ""$id"": ""2"", + ""Name"": ""John Smith"", + ""Spouse"": { + ""$id"": ""3"", + ""Name"": ""Mary Sue"", + ""Spouse"": { + ""$ref"": ""2"" + } + } + }, + ""person2"": { + ""$ref"": ""3"" + }, + ""person3"": { + ""$ref"": ""2"" + } +}"); + + IDeserializationCallbackTestObject obj2 = JsonConvert.DeserializeObject(json); + + Assert.AreEqual(obj._stringValue, obj2._stringValue); + Assert.AreEqual(obj._intValue, obj2._intValue); + Assert.AreEqual(obj._person1.Name, obj2._person1.Name); + Assert.AreEqual(obj._person2.Name, obj2._person2.Name); + Assert.AreEqual(obj._person3.Name, obj2._person3.Name); + Assert.AreEqual(obj2._person1, obj2._person3); + Assert.AreEqual(obj2._person1.Spouse, obj2._person2); + } #endif [Test] diff --git a/Src/Newtonsoft.Json/Serialization/JsonFormatterConverter.cs b/Src/Newtonsoft.Json/Serialization/JsonFormatterConverter.cs index b4135d8a..cc68e9d0 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonFormatterConverter.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonFormatterConverter.cs @@ -34,13 +34,18 @@ namespace Newtonsoft.Json.Serialization { internal class JsonFormatterConverter : IFormatterConverter { - private readonly JsonSerializer _serializer; + private readonly JsonSerializerInternalReader _reader; + private readonly JsonISerializableContract _contract; + private readonly JsonProperty _member; - public JsonFormatterConverter(JsonSerializer serializer) + public JsonFormatterConverter(JsonSerializerInternalReader reader, JsonISerializableContract contract, JsonProperty member) { - ValidationUtils.ArgumentNotNull(serializer, "serializer"); + ValidationUtils.ArgumentNotNull(reader, "serializer"); + ValidationUtils.ArgumentNotNull(contract, "contract"); - _serializer = serializer; + _reader = reader; + _contract = contract; + _member = member; } private T GetTokenValue(object value) @@ -59,7 +64,7 @@ namespace Newtonsoft.Json.Serialization if (token == null) throw new ArgumentException("Value is not a JToken.", "value"); - return _serializer.Deserialize(token.CreateReader(), type); + return _reader.CreateISerializableItem(token, type, _contract, _member); } public object Convert(object value, TypeCode typeCode) diff --git a/Src/Newtonsoft.Json/Serialization/JsonISerializableContract.cs b/Src/Newtonsoft.Json/Serialization/JsonISerializableContract.cs index 50d0d4e6..dab49ea4 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonISerializableContract.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonISerializableContract.cs @@ -25,6 +25,7 @@ #if !(NETFX_CORE || PORTABLE || PORTABLE40) using System; +using System.Runtime.Serialization; namespace Newtonsoft.Json.Serialization { @@ -39,6 +40,12 @@ namespace Newtonsoft.Json.Serialization /// The ISerializable object constructor. public ObjectConstructor ISerializableCreator { get; set; } + /// + /// Gets whether the type implements IDeserializationCallback. + /// + /// True if the type implements IDeserialiationCallback, otherwise false. + public bool HasDeserializationCallback { get; private set; } + /// /// Initializes a new instance of the class. /// @@ -47,6 +54,7 @@ namespace Newtonsoft.Json.Serialization : base(underlyingType) { ContractType = JsonContractType.Serializable; + HasDeserializationCallback = typeof(IDeserializationCallback).IsAssignableFrom(underlyingType); } } } diff --git a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs index 73fe9718..f4b78e0f 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs @@ -58,9 +58,6 @@ namespace Newtonsoft.Json.Serialization } private JsonSerializerProxy _internalSerializer; -#if !(NETFX_CORE || PORTABLE40 || PORTABLE) - private JsonFormatterConverter _formatterConverter; -#endif public JsonSerializerInternalReader(JsonSerializer serializer) : base(serializer) @@ -193,16 +190,6 @@ namespace Newtonsoft.Json.Serialization return _internalSerializer; } -#if !(NETFX_CORE || PORTABLE40 || PORTABLE) - private JsonFormatterConverter GetFormatterConverter() - { - if (_formatterConverter == null) - _formatterConverter = new JsonFormatterConverter(GetInternalSerializer()); - - return _formatterConverter; - } -#endif - private JToken CreateJToken(JsonReader reader, JsonContract contract) { ValidationUtils.ArgumentNotNull(reader, "reader"); @@ -1468,7 +1455,7 @@ namespace Newtonsoft.Json.Serialization if (TraceWriter != null && TraceWriter.LevelFilter >= TraceLevel.Info) TraceWriter.Trace(TraceLevel.Info, JsonPosition.FormatMessage(reader as IJsonLineInfo, reader.Path, "Deserializing {0} using ISerializable constructor.".FormatWith(CultureInfo.InvariantCulture, contract.UnderlyingType)), null); - SerializationInfo serializationInfo = new SerializationInfo(contract.UnderlyingType, GetFormatterConverter()); + SerializationInfo serializationInfo = new SerializationInfo(contract.UnderlyingType, new JsonFormatterConverter(this, contract, member)); bool finished = false; do @@ -1479,17 +1466,7 @@ namespace Newtonsoft.Json.Serialization string memberName = reader.Value.ToString(); if (!reader.Read()) throw JsonSerializationException.Create(reader, "Unexpected end when setting {0}'s value.".FormatWith(CultureInfo.InvariantCulture, memberName)); - - if (reader.TokenType == JsonToken.StartObject) - { - // this will read any potential type names embedded in json - object o = CreateObject(reader, null, null, null, contract, member, null); - serializationInfo.AddValue(memberName, o); - } - else - { - serializationInfo.AddValue(memberName, JToken.ReadFrom(reader)); - } + serializationInfo.AddValue(memberName, JToken.ReadFrom(reader)); break; case JsonToken.Comment: break; @@ -1516,8 +1493,33 @@ namespace Newtonsoft.Json.Serialization OnDeserializing(reader, contract, createdObject); OnDeserialized(reader, contract, createdObject); + // According to documentation this method is called after the entire object graph is deserialized, + // however doing this isn't compatible with the type of serialization model used by JSON. + // The creation of an object with $id would end up postponed, causing references to it in the object graph + // to fail. The .NET Framework shows that it's valid to call this on-demand before that point if another + // object depends on this one, which is what will be done here in every case. + if (contract.HasDeserializationCallback) + ((IDeserializationCallback) createdObject).OnDeserialization(null); + return createdObject; } + + internal object CreateISerializableItem(JToken token, Type type, JsonISerializableContract contract, JsonProperty member) + { + JsonContract itemContract = GetContractSafe(type); + JsonConverter itemConverter = GetConverter(itemContract, null, contract, member); + + JsonReader tokenReader = token.CreateReader(); + CheckedRead(tokenReader); // Move to first token + + object result; + if (itemConverter != null && itemConverter.CanRead) + result = DeserializeConvertable(itemConverter, tokenReader, type, null); + else + result = CreateValueInternal(tokenReader, type, itemContract, null, contract, member, null); + + return result; + } #endif #if !(NET35 || NET20 || PORTABLE40) diff --git a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs index 84c5358f..24a65a7d 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs @@ -749,7 +749,12 @@ To fix this error either change the environment to be fully trusted, change the { JsonContract valueContract = GetContractSafe(serializationEntry.Value); - if (CheckForCircularReference(writer, serializationEntry.Value, null, valueContract, contract, member)) + if (ShouldWriteReference(serializationEntry.Value, null, valueContract, contract, member)) + { + writer.WritePropertyName(serializationEntry.Name); + WriteReference(writer, serializationEntry.Value); + } + else if (CheckForCircularReference(writer, serializationEntry.Value, null, valueContract, contract, member)) { writer.WritePropertyName(serializationEntry.Name); SerializeValue(writer, serializationEntry.Value, valueContract, null, contract, member); From 93e1113fa24788b6175c3359b0146bb634196aea Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Tue, 11 Nov 2014 19:01:27 +1300 Subject: [PATCH 2/2] -Refactor --- .../Properties/AssemblyInfo.cs | 2 +- .../Properties/AssemblyInfo.cs | 2 +- .../Serialization/JsonSerializerTest.cs | 41 ++++++++++++------- .../Properties/AssemblyInfo.cs | 2 +- .../JsonISerializableContract.cs | 8 ---- .../JsonSerializerInternalReader.cs | 8 ---- 6 files changed, 29 insertions(+), 34 deletions(-) diff --git a/Src/Newtonsoft.Json.TestConsole/Properties/AssemblyInfo.cs b/Src/Newtonsoft.Json.TestConsole/Properties/AssemblyInfo.cs index 9be3690c..aa32efb8 100644 --- a/Src/Newtonsoft.Json.TestConsole/Properties/AssemblyInfo.cs +++ b/Src/Newtonsoft.Json.TestConsole/Properties/AssemblyInfo.cs @@ -33,4 +33,4 @@ using System.Runtime.InteropServices; // by using the '*' as shown below: // [assembly: AssemblyVersion("6.0.0.0")] [assembly: AssemblyVersion("6.0.0.0")] -[assembly: AssemblyFileVersion("6.0.6.17905")] +[assembly: AssemblyFileVersion("6.0.6.17911")] diff --git a/Src/Newtonsoft.Json.Tests/Properties/AssemblyInfo.cs b/Src/Newtonsoft.Json.Tests/Properties/AssemblyInfo.cs index be30b977..b22f60d7 100644 --- a/Src/Newtonsoft.Json.Tests/Properties/AssemblyInfo.cs +++ b/Src/Newtonsoft.Json.Tests/Properties/AssemblyInfo.cs @@ -74,4 +74,4 @@ using System.Security; // by using the '*' as shown below: [assembly: AssemblyVersion("6.0.0.0")] -[assembly: AssemblyFileVersion("6.0.6.17905")] +[assembly: AssemblyFileVersion("6.0.6.17911")] diff --git a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs index afcc9cff..f3ba31f1 100644 --- a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs +++ b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs @@ -700,16 +700,17 @@ namespace Newtonsoft.Json.Tests.Serialization Assert.AreEqual(ratio.Numerator, ratio2.Numerator); } - public class IDeserializationCallbackTestObject : ISerializable, IDeserializationCallback + public class PreserveReferencesCallbackTestObject : ISerializable { internal string _stringValue; internal int _intValue; internal PersonReference _person1; internal PersonReference _person2; internal PersonReference _person3; + internal PreserveReferencesCallbackTestObject _parent; internal SerializationInfo _serializationInfo; - public IDeserializationCallbackTestObject(string stringValue, int intValue, PersonReference p1, PersonReference p2, PersonReference p3) + public PreserveReferencesCallbackTestObject(string stringValue, int intValue, PersonReference p1, PersonReference p2, PersonReference p3) { _stringValue = stringValue; _intValue = intValue; @@ -718,7 +719,7 @@ namespace Newtonsoft.Json.Tests.Serialization _person3 = p3; } - protected IDeserializationCallbackTestObject(SerializationInfo info, StreamingContext context) + protected PreserveReferencesCallbackTestObject(SerializationInfo info, StreamingContext context) { _serializationInfo = info; } @@ -730,25 +731,28 @@ namespace Newtonsoft.Json.Tests.Serialization info.AddValue("person1", _person1, typeof(PersonReference)); info.AddValue("person2", _person2, typeof(PersonReference)); info.AddValue("person3", _person3, typeof(PersonReference)); + info.AddValue("parent", _parent, typeof(PreserveReferencesCallbackTestObject)); } - void IDeserializationCallback.OnDeserialization(object sender) + [OnDeserialized] + private void OnDeserializedMethod(StreamingContext context) { if (_serializationInfo == null) return; _stringValue = _serializationInfo.GetString("stringValue"); _intValue = _serializationInfo.GetInt32("intValue"); - _person1 = (PersonReference) _serializationInfo.GetValue("person1", typeof(PersonReference)); - _person2 = (PersonReference) _serializationInfo.GetValue("person2", typeof(PersonReference)); - _person3 = (PersonReference) _serializationInfo.GetValue("person3", typeof(PersonReference)); + _person1 = (PersonReference)_serializationInfo.GetValue("person1", typeof(PersonReference)); + _person2 = (PersonReference)_serializationInfo.GetValue("person2", typeof(PersonReference)); + _person3 = (PersonReference)_serializationInfo.GetValue("person3", typeof(PersonReference)); + _parent = (PreserveReferencesCallbackTestObject)_serializationInfo.GetValue("parent", typeof(PreserveReferencesCallbackTestObject)); _serializationInfo = null; } } [Test] - public void DeserializeIDeserializationCallback() + public void PreserveReferencesCallbackTest() { var p1 = new PersonReference { @@ -762,7 +766,8 @@ namespace Newtonsoft.Json.Tests.Serialization p1.Spouse = p2; p2.Spouse = p1; - var obj = new IDeserializationCallbackTestObject("string!", 42, p1, p2, p1); + var obj = new PreserveReferencesCallbackTestObject("string!", 42, p1, p2, p1); + obj._parent = obj; var settings = new JsonSerializerSettings { @@ -792,10 +797,13 @@ namespace Newtonsoft.Json.Tests.Serialization }, ""person3"": { ""$ref"": ""2"" + }, + ""parent"": { + ""$ref"": ""1"" } }"); - IDeserializationCallbackTestObject obj2 = JsonConvert.DeserializeObject(json); + PreserveReferencesCallbackTestObject obj2 = JsonConvert.DeserializeObject(json); Assert.AreEqual(obj._stringValue, obj2._stringValue); Assert.AreEqual(obj._intValue, obj2._intValue); @@ -804,6 +812,8 @@ namespace Newtonsoft.Json.Tests.Serialization Assert.AreEqual(obj._person3.Name, obj2._person3.Name); Assert.AreEqual(obj2._person1, obj2._person3); Assert.AreEqual(obj2._person1.Spouse, obj2._person2); + Assert.AreEqual(obj2._person2.Spouse, obj2._person1); + Assert.AreEqual(obj2._parent, obj2); } #endif @@ -4041,9 +4051,9 @@ Path '', line 1, position 1."); JsonTypeReflector.SetFullyTrusted(false); JsonConvert.DeserializeObject("{booleanValue:true}"); - }, @"Type 'Newtonsoft.Json.Tests.Serialization.JsonSerializerTest+ISerializableTestObject' implements ISerializable but cannot be deserialized using the ISerializable interface because the current application is not fully trusted and ISerializable can expose secure data. -To fix this error either change the environment to be fully trusted, change the application to not deserialize the type, add JsonObjectAttribute to the type or change the JsonSerializer setting ContractResolver to use a new DefaultContractResolver with IgnoreSerializableInterface set to true. -Path 'booleanValue', line 1, position 14."); + }, @"Type 'Newtonsoft.Json.Tests.Serialization.JsonSerializerTest+ISerializableTestObject' implements ISerializable but cannot be deserialized using the ISerializable interface because the current application is not fully trusted and ISerializable can expose secure data." + Environment.NewLine + + @"To fix this error either change the environment to be fully trusted, change the application to not deserialize the type, add JsonObjectAttribute to the type or change the JsonSerializer setting ContractResolver to use a new DefaultContractResolver with IgnoreSerializableInterface set to true." + Environment.NewLine + + @"Path 'booleanValue', line 1, position 14."); } finally { @@ -4062,8 +4072,9 @@ Path 'booleanValue', line 1, position 14."); ISerializableTestObject value = new ISerializableTestObject("string!", 0, default(DateTimeOffset), null); JsonConvert.SerializeObject(value); - }, @"Type 'Newtonsoft.Json.Tests.Serialization.JsonSerializerTest+ISerializableTestObject' implements ISerializable but cannot be serialized using the ISerializable interface because the current application is not fully trusted and ISerializable can expose secure data. -To fix this error either change the environment to be fully trusted, change the application to not deserialize the type, add JsonObjectAttribute to the type or change the JsonSerializer setting ContractResolver to use a new DefaultContractResolver with IgnoreSerializableInterface set to true. Path ''."); + }, @"Type 'Newtonsoft.Json.Tests.Serialization.JsonSerializerTest+ISerializableTestObject' implements ISerializable but cannot be serialized using the ISerializable interface because the current application is not fully trusted and ISerializable can expose secure data." + Environment.NewLine + + @"To fix this error either change the environment to be fully trusted, change the application to not deserialize the type, add JsonObjectAttribute to the type or change the JsonSerializer setting ContractResolver to use a new DefaultContractResolver with IgnoreSerializableInterface set to true." + Environment.NewLine + + @"Path ''."); } finally { diff --git a/Src/Newtonsoft.Json/Properties/AssemblyInfo.cs b/Src/Newtonsoft.Json/Properties/AssemblyInfo.cs index a857c14b..ae3623f9 100644 --- a/Src/Newtonsoft.Json/Properties/AssemblyInfo.cs +++ b/Src/Newtonsoft.Json/Properties/AssemblyInfo.cs @@ -91,5 +91,5 @@ using System.Security; // by using the '*' as shown below: [assembly: AssemblyVersion("6.0.0.0")] -[assembly: AssemblyFileVersion("6.0.6.17905")] +[assembly: AssemblyFileVersion("6.0.6.17911")] [assembly: CLSCompliant(true)] diff --git a/Src/Newtonsoft.Json/Serialization/JsonISerializableContract.cs b/Src/Newtonsoft.Json/Serialization/JsonISerializableContract.cs index dab49ea4..d69e5fb5 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonISerializableContract.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonISerializableContract.cs @@ -40,12 +40,6 @@ namespace Newtonsoft.Json.Serialization /// The ISerializable object constructor. public ObjectConstructor ISerializableCreator { get; set; } - /// - /// Gets whether the type implements IDeserializationCallback. - /// - /// True if the type implements IDeserialiationCallback, otherwise false. - public bool HasDeserializationCallback { get; private set; } - /// /// Initializes a new instance of the class. /// @@ -54,9 +48,7 @@ namespace Newtonsoft.Json.Serialization : base(underlyingType) { ContractType = JsonContractType.Serializable; - HasDeserializationCallback = typeof(IDeserializationCallback).IsAssignableFrom(underlyingType); } } } - #endif \ No newline at end of file diff --git a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs index f4b78e0f..e90c7e4d 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs @@ -1493,14 +1493,6 @@ namespace Newtonsoft.Json.Serialization OnDeserializing(reader, contract, createdObject); OnDeserialized(reader, contract, createdObject); - // According to documentation this method is called after the entire object graph is deserialized, - // however doing this isn't compatible with the type of serialization model used by JSON. - // The creation of an object with $id would end up postponed, causing references to it in the object graph - // to fail. The .NET Framework shows that it's valid to call this on-demand before that point if another - // object depends on this one, which is what will be done here in every case. - if (contract.HasDeserializationCallback) - ((IDeserializationCallback) createdObject).OnDeserialization(null); - return createdObject; }