From 8c545b0bc02f3a43e9c2d4fa1dc1c250abe13b9e Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sun, 8 Mar 2015 16:20:54 +1300 Subject: [PATCH] -Fixed IsoDateTimeConverter on DateTime properties with DateTimeFormatHandling.DateTimeOffset -Fixed preserving object references with read only properties --- .../Serialization/JsonSerializerTest.cs | 110 ++++++++++++++++-- .../PreserveReferencesHandlingTests.cs | 89 ++++++++++++++ .../Converters/IsoDateTimeConverter.cs | 4 +- .../JsonSerializerInternalWriter.cs | 6 +- 4 files changed, 199 insertions(+), 10 deletions(-) diff --git a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs index 960447c3..f3c44c45 100644 --- a/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs +++ b/Src/Newtonsoft.Json.Tests/Serialization/JsonSerializerTest.cs @@ -332,7 +332,7 @@ namespace Newtonsoft.Json.Tests.Serialization { Error = (s, a) => a.ErrorContext.Handled = true }); - + Assert.AreEqual(0, l.ChildId); } #endif @@ -394,7 +394,7 @@ namespace Newtonsoft.Json.Tests.Serialization public class ChildClassVirtual : BaseClassVirtual { - public virtual new bool IsTransient { get; set; } + public new virtual bool IsTransient { get; set; } } [Test] @@ -413,7 +413,7 @@ namespace Newtonsoft.Json.Tests.Serialization public class ResponseWithNewGenericPropertyVirtual : SimpleResponse { - public virtual new T Data { get; set; } + public new virtual T Data { get; set; } } public class ResponseWithNewGenericPropertyOverride : ResponseWithNewGenericPropertyVirtual @@ -1000,7 +1000,7 @@ namespace Newtonsoft.Json.Tests.Serialization JObject oo = new JObject { - {"title", v} + { "title", v } }; string output = o.ToString(); @@ -1711,6 +1711,7 @@ keyword such as type of business."" { ExceptionAssert.Throws(() => { JsonConvert.SerializeObject(new BadJsonPropertyClass()); }, @"A member with the name 'pie' already exists on 'Newtonsoft.Json.Tests.TestObjects.BadJsonPropertyClass'. Use the JsonPropertyAttribute to specify another name."); } + #if !NET20 [Test] public void Unicode() @@ -3280,7 +3281,8 @@ Path '', line 1, position 1."); ExceptionAssert.Throws( () => { JsonConvert.DeserializeObject(json); }, - new [] { + new[] + { "Unable to cast object of type 'Newtonsoft.Json.Linq.JArray' to type 'Newtonsoft.Json.Linq.JObject'.", "Cannot cast from source type to destination type." // mono }); @@ -3315,8 +3317,8 @@ Path '', line 1, position 1."); catch (JsonSerializationException ex) { Assert.IsTrue(ex.Message.StartsWith(@"Cannot deserialize the current JSON object (e.g. {""name"":""value""}) into type 'System.Collections.Generic.List`1[Newtonsoft.Json.Tests.TestObjects.Person]' because the type requires a JSON array (e.g. [1,2,3]) to deserialize correctly." + Environment.NewLine + -@"To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object." + Environment.NewLine + -@"Path ''")); + @"To fix this error either change the JSON to a JSON array (e.g. [1,2,3]) or change the deserialized type so that it is a normal .NET type (e.g. not a primitive type like integer, not a collection type like an array or List) that can be deserialized from a JSON object. JsonObjectAttribute can also be added to the type to force it to deserialize from a JSON object." + Environment.NewLine + + @"Path ''")); } } @@ -3877,6 +3879,100 @@ Path '', line 1, position 1."); }", json); } + public class DateTimeOffsetWrapper + { + public DateTimeOffset DateTimeOffsetValue { get; set; } + + public DateTime DateTimeValue { get; set; } + } + + [Test] + public void DeserializeDateTimeOffsetAndDateTime() + { + string jsonIsoText = + @"{""DateTimeOffsetValue"":""2012-02-25T19:55:50.6095676+00:00"", ""DateTimeValue"":""2012-02-25T19:55:50.6095676+00:00""}"; + + DateTimeOffsetWrapper cISO = JsonConvert.DeserializeObject(jsonIsoText, new JsonSerializerSettings + { + DateParseHandling = DateParseHandling.DateTimeOffset, + Converters = + { + new IsoDateTimeConverter() + } + }); + DateTimeOffsetWrapper c = JsonConvert.DeserializeObject(jsonIsoText, new JsonSerializerSettings + { + DateParseHandling = DateParseHandling.DateTimeOffset + }); + + Assert.AreEqual(c.DateTimeOffsetValue, cISO.DateTimeOffsetValue); + } + + [Test] + public void CircularConstructorDeserialize() + { + CircularConstructor1 c1 = new CircularConstructor1(null) + { + StringProperty = "Value!" + }; + + CircularConstructor2 c2 = new CircularConstructor2(null) + { + IntProperty = 1 + }; + + c1.C2 = c2; + c2.C1 = c1; + + string json = JsonConvert.SerializeObject(c1, new JsonSerializerSettings + { + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + Formatting = Formatting.Indented + }); + + Assert.AreEqual(@"{ + ""C2"": { + ""IntProperty"": 1 + }, + ""StringProperty"": ""Value!"" +}", json); + + CircularConstructor1 newC1 = JsonConvert.DeserializeObject(@"{ + ""C2"": { + ""IntProperty"": 1, + ""C1"": {} + }, + ""StringProperty"": ""Value!"" +}"); + + Assert.AreEqual("Value!", newC1.StringProperty); + Assert.AreEqual(1, newC1.C2.IntProperty); + Assert.AreEqual(null, newC1.C2.C1.StringProperty); + Assert.AreEqual(null, newC1.C2.C1.C2); + } + + public class CircularConstructor1 + { + public CircularConstructor2 C2 { get; internal set; } + public string StringProperty { get; set; } + + public CircularConstructor1(CircularConstructor2 c2) + { + C2 = c2; + } + } + + public class CircularConstructor2 + { + public CircularConstructor1 C1 { get; internal set; } + public int IntProperty { get; set; } + + public CircularConstructor2(CircularConstructor1 c1) + { + C1 = c1; + } + } + public class TestClass { public string Key { get; set; } diff --git a/Src/Newtonsoft.Json.Tests/Serialization/PreserveReferencesHandlingTests.cs b/Src/Newtonsoft.Json.Tests/Serialization/PreserveReferencesHandlingTests.cs index 4bbe6214..713b74a9 100644 --- a/Src/Newtonsoft.Json.Tests/Serialization/PreserveReferencesHandlingTests.cs +++ b/Src/Newtonsoft.Json.Tests/Serialization/PreserveReferencesHandlingTests.cs @@ -47,6 +47,95 @@ namespace Newtonsoft.Json.Tests.Serialization [TestFixture] public class PreserveReferencesHandlingTests : TestFixtureBase { + public class Parent + { + public Child ReadOnlyChild + { + get { return Child1; } + } + + public Child Child1 { get; set; } + public Child Child2 { get; set; } + + public IList ReadOnlyList + { + get { return List1; } + } + + public IList List1 { get; set; } + public IList List2 { get; set; } + } + + public class Child + { + public string PropertyName { get; set; } + } + + public void SerializeReadOnlyProperty() + { + Child c = new Child + { + PropertyName = "value?" + }; + IList l = new List + { + "value!" + }; + Parent p = new Parent + { + Child1 = c, + Child2 = c, + List1 = l, + List2 = l + }; + + string json = JsonConvert.SerializeObject(p, new JsonSerializerSettings + { + Formatting = Formatting.Indented, + PreserveReferencesHandling = PreserveReferencesHandling.All + }); + + Assert.AreEqual(@"{ + ""$id"": ""1"", + ""ReadOnlyChild"": { + ""PropertyName"": ""value?"" + }, + ""Child1"": { + ""$id"": ""2"", + ""PropertyName"": ""value?"" + }, + ""Child2"": { + ""$ref"": ""2"" + }, + ""ReadOnlyList"": [ + ""value!"" + ], + ""List1"": { + ""$id"": ""3"", + ""$values"": [ + ""value!"" + ] + }, + ""List2"": { + ""$ref"": ""3"" + } +}", json); + + Parent newP = JsonConvert.DeserializeObject(json, new JsonSerializerSettings + { + Formatting = Formatting.Indented, + PreserveReferencesHandling = PreserveReferencesHandling.All + }); + + Assert.AreEqual("value?", newP.Child1.PropertyName); + Assert.AreEqual(newP.Child1, newP.Child2); + Assert.AreEqual(newP.Child1, newP.ReadOnlyChild); + + Assert.AreEqual("value!", newP.List1[0]); + Assert.AreEqual(newP.List1, newP.List2); + Assert.AreEqual(newP.List1, newP.ReadOnlyList); + } + [Test] public void SerializeDictionarysWithPreserveObjectReferences() { diff --git a/Src/Newtonsoft.Json/Converters/IsoDateTimeConverter.cs b/Src/Newtonsoft.Json/Converters/IsoDateTimeConverter.cs index a7d78e88..85f6df97 100644 --- a/Src/Newtonsoft.Json/Converters/IsoDateTimeConverter.cs +++ b/Src/Newtonsoft.Json/Converters/IsoDateTimeConverter.cs @@ -138,10 +138,10 @@ namespace Newtonsoft.Json.Converters { #if !NET20 if (t == typeof(DateTimeOffset)) - return reader.Value is DateTimeOffset ? reader.Value : new DateTimeOffset((DateTime)reader.Value); + return (reader.Value is DateTimeOffset) ? reader.Value : new DateTimeOffset((DateTime)reader.Value); #endif - return reader.Value; + return (reader.Value is DateTime) ? reader.Value : ((DateTimeOffset)reader.Value).DateTime; } if (reader.TokenType != JsonToken.String) diff --git a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs index 7c0013d6..1728b7c0 100644 --- a/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs +++ b/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalWriter.cs @@ -497,7 +497,8 @@ namespace Newtonsoft.Json.Serialization writer.WriteStartObject(); bool isReference = ResolveIsReference(contract, member, collectionContract, containerProperty) ?? HasFlag(Serializer._preserveReferencesHandling, PreserveReferencesHandling.Objects); - if (isReference) + // don't make readonly fields the referenced value because they can't be deserialized to + if (isReference && (member == null || member.Writable)) { WriteReferenceIdProperty(writer, contract.UnderlyingType, value); } @@ -701,6 +702,9 @@ namespace Newtonsoft.Json.Serialization private bool WriteStartArray(JsonWriter writer, object values, JsonArrayContract contract, JsonProperty member, JsonContainerContract containerContract, JsonProperty containerProperty) { bool isReference = ResolveIsReference(contract, member, containerContract, containerProperty) ?? HasFlag(Serializer._preserveReferencesHandling, PreserveReferencesHandling.Arrays); + // don't make readonly fields the referenced value because they can't be deserialized to + isReference = (isReference && (member == null || member.Writable)); + bool includeTypeDetails = ShouldWriteType(TypeNameHandling.Arrays, contract, member, containerContract, containerProperty); bool writeMetadataObject = isReference || includeTypeDetails;