From ae254ade2802cc10d1bbdb2da13b1cc191365332 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Wed, 24 Sep 2014 12:43:45 -0400 Subject: [PATCH 01/17] Support white-space when parsing __type info --- .../Common/DeserializeType.cs | 13 ++++- .../Json/JsonTypeSerializer.cs | 8 +-- src/ServiceStack.Text/Json/JsonUtils.cs | 2 + .../JsonTests/TypeInfoTests.cs | 49 +++++++++++++++++++ .../ServiceStack.Text.Tests.csproj | 1 + 5 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 tests/ServiceStack.Text.Tests/JsonTests/TypeInfoTests.cs diff --git a/src/ServiceStack.Text/Common/DeserializeType.cs b/src/ServiceStack.Text/Common/DeserializeType.cs index e23b25c61..25e8cf3bb 100644 --- a/src/ServiceStack.Text/Common/DeserializeType.cs +++ b/src/ServiceStack.Text/Common/DeserializeType.cs @@ -77,8 +77,17 @@ public static object ObjectStringToType(string strType) public static Type ExtractType(string strType) { var typeAttrInObject = Serializer.TypeAttrInObject; - if (strType != null - && strType.Length > typeAttrInObject.Length + if (strType == null || strType.Length <= 1) return null; + + var hasWhitespace = JsonUtils.WhiteSpaceChars.Contains(strType[1]); + if (hasWhitespace) + { + var pos = strType.IndexOf('"'); + if (pos >= 0) + strType = "{" + strType.Substring(pos); + } + + if (strType.Length > typeAttrInObject.Length && strType.Substring(0, typeAttrInObject.Length) == typeAttrInObject) { var propIndex = typeAttrInObject.Length; diff --git a/src/ServiceStack.Text/Json/JsonTypeSerializer.cs b/src/ServiceStack.Text/Json/JsonTypeSerializer.cs index fb3809713..732f5cb76 100644 --- a/src/ServiceStack.Text/Json/JsonTypeSerializer.cs +++ b/src/ServiceStack.Text/Json/JsonTypeSerializer.cs @@ -42,10 +42,10 @@ internal static string GetTypeAttrInObject(string typeAttr) static JsonTypeSerializer() { - WhiteSpaceFlags[' '] = true; - WhiteSpaceFlags['\t'] = true; - WhiteSpaceFlags['\r'] = true; - WhiteSpaceFlags['\n'] = true; + foreach (var c in JsonUtils.WhiteSpaceChars) + { + WhiteSpaceFlags[c] = true; + } } public WriteObjectDelegate GetWriteFn() diff --git a/src/ServiceStack.Text/Json/JsonUtils.cs b/src/ServiceStack.Text/Json/JsonUtils.cs index 442b88b12..5197682da 100644 --- a/src/ServiceStack.Text/Json/JsonUtils.cs +++ b/src/ServiceStack.Text/Json/JsonUtils.cs @@ -11,6 +11,8 @@ public static class JsonUtils public const string True = "true"; public const string False = "false"; + public static char[] WhiteSpaceChars = new[] { ' ', '\t', '\r', '\n' }; + static readonly char[] EscapeChars = new[] { QuoteChar, '\n', '\r', '\t', '"', '\\', '\f', '\b', diff --git a/tests/ServiceStack.Text.Tests/JsonTests/TypeInfoTests.cs b/tests/ServiceStack.Text.Tests/JsonTests/TypeInfoTests.cs new file mode 100644 index 000000000..a359bb418 --- /dev/null +++ b/tests/ServiceStack.Text.Tests/JsonTests/TypeInfoTests.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; + +namespace ServiceStack.Text.Tests.JsonTests +{ + public class TypeInfoTests + { + class MyClass : IComparable + { + public int CompareTo(object obj) + { + return 0; + } + } + + [Test] + [TestCase("[{'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}]")] + [TestCase("[{ '__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}]")] + [TestCase("[{\n'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}]")] + [TestCase("[{\t'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}]")] + [TestCase("[ {'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}]")] + [TestCase("[\n{'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}]")] + [TestCase("[\t{'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}]")] + [TestCase("[ { '__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}]")] + [TestCase("[\n{\n'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}]")] + [TestCase("[\t{\t'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}]")] + [TestCase("[{'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'} ]")] + [TestCase("[{ '__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}\t]")] + [TestCase("[{\n'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}\n]")] + [TestCase("[{\t'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass' }]")] + [TestCase("[ {'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'\t}]")] + [TestCase("[\n{'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'\n}]")] + [TestCase("[\t{'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'\t}\n]")] + [TestCase("[ { '__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass', }]")] + [TestCase("[\n{\n'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}]")] + [TestCase("[\t{\t'__type':'ServiceStack.Text.Tests.JsonTests.TypeInfoTests+MyClass'}]")] + public void TypeAttrInObject(string json) + { + json = json.Replace('\'', '"'); + var deserDto = JsonSerializer.DeserializeFromString>(json); + Console.WriteLine(json); + Assert.IsNotNull(deserDto); + Assert.AreEqual(1, deserDto.Count); + Assert.IsNotNull(deserDto[0]); + } + + } +} \ No newline at end of file diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index dfaf6e788..6ee1b4611 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -164,6 +164,7 @@ + From 098d1d8976b412ca18170ba8f2fae8ce7fd955fa Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Mon, 20 Oct 2014 14:49:51 +0200 Subject: [PATCH 02/17] This change allows to work with existence of the attribute on the Json message in same way as dynamic languages do. (after deserialization to JavaScript for example you could test field existence by hasAttribute() function). Changes: - support for conditional serialization with method ShouldSerializeByName on DTO found by convention. - The difference to existing ShouldSerializeX is that it could handle all fields not just one. - It returns nullable bool with following meaning: True- should be on the json. False - should not be on the JSon. Null - use behavior defined by configuration and DataMemberAttribute - support for attribute on wire detection with method OnDeserializingMember on DTO found by convention. - it complements ShouldSerializeByName and allows to implement full roundtrip of existence of the field on the JSon message. - it's designed in a way which allows event handler to change the deserialized object value before it's assigned to target field. - it will give raw json string for fields which don't have member on the DTO, allowing to implement parallel to IExtensibleDataObject for Json. It allows survival and forwarding of unknown attributes. --- .../Common/DeserializeType.cs | 4 +- .../Common/DeserializeTypeRefJson.cs | 16 +++- .../Common/DeserializeTypeRefJsv.cs | 16 +++- src/ServiceStack.Text/Common/WriteType.cs | 61 ++++++++++---- src/ServiceStack.Text/ReflectionExtensions.cs | 11 +++ src/ServiceStack.Text/TypeConfig.cs | 8 ++ .../ConditionalSerializationTests.cs | 84 ++++++++++++++++++- 7 files changed, 181 insertions(+), 19 deletions(-) diff --git a/src/ServiceStack.Text/Common/DeserializeType.cs b/src/ServiceStack.Text/Common/DeserializeType.cs index 25e8cf3bb..e694a2b65 100644 --- a/src/ServiceStack.Text/Common/DeserializeType.cs +++ b/src/ServiceStack.Text/Common/DeserializeType.cs @@ -40,8 +40,8 @@ public static ParseStringDelegate GetParseMethod(TypeConfig typeConfig) return value => ctorFn(); return typeof(TSerializer) == typeof(Json.JsonTypeSerializer) - ? (ParseStringDelegate)(value => DeserializeTypeRefJson.StringToType(type, value, ctorFn, map)) - : value => DeserializeTypeRefJsv.StringToType(type, value, ctorFn, map); + ? (ParseStringDelegate)(value => DeserializeTypeRefJson.StringToType(typeConfig, value, ctorFn, map)) + : value => DeserializeTypeRefJsv.StringToType(typeConfig, value, ctorFn, map); } public static object ObjectStringToType(string strType) diff --git a/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs index 3068db8e3..a240eadf2 100644 --- a/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs +++ b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs @@ -52,12 +52,13 @@ internal static class DeserializeTypeRefJson private static readonly JsonTypeSerializer Serializer = (JsonTypeSerializer)JsonTypeSerializer.Instance; internal static object StringToType( - Type type, + TypeConfig typeConfig, string strType, EmptyCtorDelegate ctorFn, Dictionary typeAccessorMap) { var index = 0; + var type = typeConfig.Type; if (strType == null) return null; @@ -140,6 +141,10 @@ internal static object StringToType( var parseFn = JsonReader.GetParseFn(propType); var propertyValue = parseFn(propertyValueStr); + if (typeConfig.OnDeserializingMember != null) + { + propertyValue = typeConfig.OnDeserializingMember(instance, propertyName, propertyValue); + } typeAccessor.SetProperty(instance, propertyValue); } @@ -167,6 +172,10 @@ internal static object StringToType( try { var propertyValue = typeAccessor.GetProperty(propertyValueStr); + if (typeConfig.OnDeserializingMember != null) + { + propertyValue = typeConfig.OnDeserializingMember(instance, propertyName, propertyValue); + } typeAccessor.SetProperty(instance, propertyValue); } catch (Exception e) @@ -175,6 +184,11 @@ internal static object StringToType( else Tracer.Instance.WriteWarning("WARN: failed to set property {0} with: {1}", propertyName, propertyValueStr); } } + else if (typeConfig.OnDeserializingMember != null) + { + // the property is not known by the DTO + typeConfig.OnDeserializingMember(instance, propertyName, propertyValueStr); + } //Serializer.EatItemSeperatorOrMapEndChar(strType, ref index); for (; index < strType.Length; index++) { var c = strType[index]; if (c >= JsonTypeSerializer.WhiteSpaceFlags.Length || !JsonTypeSerializer.WhiteSpaceFlags[c]) break; } //Whitespace inline diff --git a/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs index 9e9355455..b481f592b 100644 --- a/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs +++ b/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs @@ -12,12 +12,13 @@ internal static class DeserializeTypeRefJsv private static readonly JsvTypeSerializer Serializer = (JsvTypeSerializer)JsvTypeSerializer.Instance; internal static object StringToType( - Type type, + TypeConfig typeConfig, string strType, EmptyCtorDelegate ctorFn, Dictionary typeAccessorMap) { var index = 0; + var type = typeConfig.Type; if (strType == null) return null; @@ -97,6 +98,10 @@ internal static object StringToType( { var parseFn = Serializer.GetParseFn(propType); var propertyValue = parseFn(propertyValueStr); + if (typeConfig.OnDeserializingMember != null) + { + propertyValue = typeConfig.OnDeserializingMember(instance, propertyName, propertyValue); + } typeAccessor.SetProperty(instance, propertyValue); } @@ -117,6 +122,10 @@ internal static object StringToType( try { var propertyValue = typeAccessor.GetProperty(propertyValueStr); + if (typeConfig.OnDeserializingMember != null) + { + propertyValue = typeConfig.OnDeserializingMember(instance, propertyName, propertyValue); + } typeAccessor.SetProperty(instance, propertyValue); } catch (Exception e) @@ -125,6 +134,11 @@ internal static object StringToType( else Tracer.Instance.WriteWarning("WARN: failed to set property {0} with: {1}", propertyName, propertyValueStr); } } + else if (typeConfig.OnDeserializingMember != null) + { + // the property is not known by the DTO + typeConfig.OnDeserializingMember(instance, propertyName, propertyValueStr); + } //Serializer.EatItemSeperatorOrMapEndChar(strType, ref index); if (index != strType.Length) index++; diff --git a/src/ServiceStack.Text/Common/WriteType.cs b/src/ServiceStack.Text/Common/WriteType.cs index 721e08d54..fd7b5b5d9 100644 --- a/src/ServiceStack.Text/Common/WriteType.cs +++ b/src/ServiceStack.Text/Common/WriteType.cs @@ -108,6 +108,16 @@ static Func GetShouldSerializeMethod(MemberInfo member) null, Type.EmptyTypes, null); return (method == null || method.ReturnType != typeof(bool)) ? null : (Func)Delegate.CreateDelegate(typeof(Func), method); } + + static Func ShouldSerializeByName(Type type) + { + var method = type.GetMethod("ShouldSerializeByName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[] { typeof(string) }, null); + return (method == null || method.ReturnType != typeof(bool?)) + ? null + : (Func)Delegate.CreateDelegate(typeof(Func), method); + } + + private static bool Init() { if (!typeof(T).IsClass() && !typeof(T).IsInterface() && !JsConfig.TreatAsRefType(typeof(T))) return false; @@ -124,6 +134,8 @@ private static bool Init() return typeof(T).IsDto(); } + var shouldSerializeByName = ShouldSerializeByName(typeof(T)); + // NOTE: very limited support for DataContractSerialization (DCS) // NOT supporting Serializable // support for DCS is intended for (re)Name of properties and Ignore by NOT having a DataMember present @@ -174,7 +186,8 @@ private static bool Init() propertyInfo.GetValueGetter(), Serializer.GetWriteFn(propertyType), propertyType.GetDefaultValue(), - shouldSerialize + shouldSerialize, + shouldSerializeByName ); } @@ -221,7 +234,8 @@ private static bool Init() fieldInfo.GetValueGetter(), Serializer.GetWriteFn(propertyType), defaultValue, - shouldSerialize + shouldSerialize, + shouldSerializeByName ); } PropertyWriters = PropertyWriters.OrderBy(x => x.propertyOrder).ToArray(); @@ -253,9 +267,10 @@ internal string PropertyName internal readonly WriteObjectDelegate WriteFn; internal readonly object DefaultValue; internal readonly Func shouldSerialize; + internal readonly Func shouldSerializeByName; public TypePropertyWriter(string propertyName, string propertyReflectedName, string propertyNameCLSFriendly, string propertyNameLowercaseUnderscore, int propertyOrder, bool propertySuppressDefaultConfig,bool propertySuppressDefaultAttribute, - Func getterFn, WriteObjectDelegate writeFn, object defaultValue, Func shouldSerialize) + Func getterFn, WriteObjectDelegate writeFn, object defaultValue, Func shouldSerialize, Func shouldSerializeByName) { this.propertyName = propertyName; this.propertyOrder = propertyOrder; @@ -269,6 +284,7 @@ public TypePropertyWriter(string propertyName, string propertyReflectedName, str this.WriteFn = writeFn; this.DefaultValue = defaultValue; this.shouldSerialize = shouldSerialize; + this.shouldSerializeByName = shouldSerializeByName; } } @@ -337,21 +353,38 @@ public static void WriteProperties(TextWriter writer, object value) var propertyWriter = PropertyWriters[index]; if (propertyWriter.shouldSerialize != null && !propertyWriter.shouldSerialize((T)value)) continue; - + bool dontSkipDefault=false; + if (propertyWriter.shouldSerializeByName != null) + { + var shouldSerialize = propertyWriter.shouldSerializeByName((T) value, propertyWriter.PropertyName); + if (shouldSerialize.HasValue) + { + if (shouldSerialize.Value) + { + dontSkipDefault = true; + } + else + { + continue; + } + } + } var propertyValue = value != null ? propertyWriter.GetterFn((T)value) : null; - - if (propertyWriter.propertySuppressDefaultAttribute && Equals(propertyWriter.DefaultValue, propertyValue)) - { - continue; - } - if ((propertyValue == null - || (propertyWriter.propertySuppressDefaultConfig && Equals(propertyWriter.DefaultValue, propertyValue))) - && !Serializer.IncludeNullValues - ) + if (!dontSkipDefault) { - continue; + if (propertyWriter.propertySuppressDefaultAttribute && Equals(propertyWriter.DefaultValue, propertyValue)) + { + continue; + } + if ((propertyValue == null + || (propertyWriter.propertySuppressDefaultConfig && Equals(propertyWriter.DefaultValue, propertyValue))) + && !Serializer.IncludeNullValues + ) + { + continue; + } } if (exclude.Any() && exclude.Contains(propertyWriter.propertyCombinedNameUpper)) continue; diff --git a/src/ServiceStack.Text/ReflectionExtensions.cs b/src/ServiceStack.Text/ReflectionExtensions.cs index 95ed990f6..28c1263b4 100644 --- a/src/ServiceStack.Text/ReflectionExtensions.cs +++ b/src/ServiceStack.Text/ReflectionExtensions.cs @@ -536,6 +536,17 @@ public static PropertyInfo[] GetSerializableProperties(this Type type) return publicReadableProperties.Where(prop => !prop.CustomAttributes(false).Any(attr => attr.GetType().Name == IgnoreDataMember)).ToArray(); } + public static Func GetOnDeserializingMember() + { + var method = typeof(T).GetMethod("OnDeserializingMember", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(string), typeof(object) }, null); + if (method == null || method.ReturnType != typeof (object)) + { + return null; + } + var ojb = (Func)Delegate.CreateDelegate(typeof(Func), method); + return (instance, memberName, value) => ojb((T)instance, memberName, value); + } + public static FieldInfo[] GetSerializableFields(this Type type) { if (type.IsDto()) { diff --git a/src/ServiceStack.Text/TypeConfig.cs b/src/ServiceStack.Text/TypeConfig.cs index bb8c1a020..29392e767 100644 --- a/src/ServiceStack.Text/TypeConfig.cs +++ b/src/ServiceStack.Text/TypeConfig.cs @@ -10,6 +10,7 @@ internal class TypeConfig internal bool EnableAnonymousFieldSetterses; internal PropertyInfo[] Properties; internal FieldInfo[] Fields; + internal Func OnDeserializingMember; internal TypeConfig(Type type) { @@ -42,6 +43,12 @@ public static bool EnableAnonymousFieldSetters set { config.EnableAnonymousFieldSetterses = value; } } + public static Func OnDeserializingMember + { + get { return config.OnDeserializingMember; } + set { config.OnDeserializingMember = value; } + } + static TypeConfig() { config = new TypeConfig(typeof(T)); @@ -54,6 +61,7 @@ static TypeConfig() Properties = properties.Where(x => x.GetIndexParameters().Length == 0).ToArray(); Fields = config.Type.GetSerializableFields().ToArray(); + OnDeserializingMember = ReflectionExtensions.GetOnDeserializingMember(); } internal static TypeConfig GetState() diff --git a/tests/ServiceStack.Text.Tests/JsonTests/ConditionalSerializationTests.cs b/tests/ServiceStack.Text.Tests/JsonTests/ConditionalSerializationTests.cs index f2eb1d92d..1f630f337 100644 --- a/tests/ServiceStack.Text.Tests/JsonTests/ConditionalSerializationTests.cs +++ b/tests/ServiceStack.Text.Tests/JsonTests/ConditionalSerializationTests.cs @@ -1,5 +1,8 @@ -using NUnit.Framework; +using System.Reflection; +using System.Runtime.Serialization; +using NUnit.Framework; using System; +using ServiceStack.Text.WP; namespace ServiceStack.Text.Tests.JsonTests { @@ -14,6 +17,7 @@ public void TestSerializeRespected() string json = JsonSerializer.SerializeToString(obj); Assert.That(json, Is.StringMatching("{\"X\":\"abc\",\"Z\":\"def\"}")); } + [Test] public void TestSerializeRespectedWithInheritance() { @@ -22,6 +26,52 @@ public void TestSerializeRespectedWithInheritance() string json = JsonSerializer.SerializeToString(obj); Assert.That(json, Is.StringMatching("{\"A\":123,\"C\":456,\"X\":\"abc\",\"Z\":\"def\"}")); } + + [Test] + public void TestSerializeHasAttributeNull() + { + var obj = new ByNameFoo { A = 123, B = 456 }; + + string json = JsonSerializer.SerializeToString(obj); + Assert.That(json, Is.StringMatching("{\"A\":123,\"B\":456}")); + } + + [Test] + public void TestSerializeHasAttributeSet() + { + var obj = new ByNameFoo {A = 123, B = 456, hasAttribute = new HashSet {"A"}}; + + string json = JsonSerializer.SerializeToString(obj); + Assert.That(json, Is.StringMatching("{\"A\":123")); + + var cpy = JsonSerializer.DeserializeFromString(json); + Assert.That(cpy.A, Is.EqualTo(obj.A)); + Assert.That(cpy.hasAttribute, Is.EqualTo(obj.hasAttribute)); + } + + [Test] + public void TestSerializeHasAttributeSetNullValue() + { + var obj = new ByNameFoo {A = 123, B = null, hasAttribute = new HashSet {"B"}}; + + string json = JsonSerializer.SerializeToString(obj); + Assert.That(json, Is.StringMatching("{\"B\":null")); + + var cpy = JsonSerializer.DeserializeFromString(json); + Assert.That(cpy.A, Is.EqualTo(0)); + Assert.That(cpy.B, Is.EqualTo(obj.B)); + Assert.That(cpy.hasAttribute, Is.EqualTo(obj.hasAttribute)); + } + + [Test] + public void OnDeserializingMemberUnknown() + { + var cpy = JsonSerializer.DeserializeFromString("{\"B\":1,\"C\":1"); + Assert.That(cpy.A, Is.EqualTo(0)); + Assert.That(cpy.B, Is.EqualTo(1)); + Assert.That(cpy.hasAttribute, Is.EqualTo(new HashSet{"B","C"})); + } + public class Foo { public string X { get; set; } // not conditional @@ -65,5 +115,37 @@ public bool ShouldSerializeC() return true; } } + + [DataContract] + public class ByNameFoo + { + [IgnoreDataMember] + public HashSet hasAttribute; + + [DataMember(EmitDefaultValue = false,IsRequired = false)] + public int A { get; set; } + + [DataMember(EmitDefaultValue = false, IsRequired = false)] + public int? B { get; set; } + + private bool? ShouldSerializeByName(string fieldName) + { + if (hasAttribute == null) + { + return null; + } + return hasAttribute.Contains(fieldName); + } + + private object OnDeserializingMember(string fieldName, object value) + { + if (hasAttribute == null) + { + hasAttribute=new HashSet(); + } + hasAttribute.Add(fieldName); + return value; + } + } } } From 29ac4ab03a20efc360c09ea657f4ee32e7cc26db Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Wed, 22 Oct 2014 18:42:01 +0200 Subject: [PATCH 03/17] including feedback from https://github.com/ServiceStack/ServiceStack.Text/pull/432 --- .../Common/DeserializeTypeRefJson.cs | 12 ++++++------ .../Common/DeserializeTypeRefJsv.cs | 12 ++++++------ src/ServiceStack.Text/Common/WriteType.cs | 6 +++--- src/ServiceStack.Text/JsConfig.cs | 12 ++++++++++++ src/ServiceStack.Text/ReflectionExtensions.cs | 7 +++---- src/ServiceStack.Text/TypeConfig.cs | 19 ++++++++++--------- .../ConditionalSerializationTests.cs | 4 ++-- 7 files changed, 42 insertions(+), 30 deletions(-) diff --git a/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs index a240eadf2..bff5c3cf6 100644 --- a/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs +++ b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs @@ -141,9 +141,9 @@ internal static object StringToType( var parseFn = JsonReader.GetParseFn(propType); var propertyValue = parseFn(propertyValueStr); - if (typeConfig.OnDeserializingMember != null) + if (typeConfig.OnDeserializing != null) { - propertyValue = typeConfig.OnDeserializingMember(instance, propertyName, propertyValue); + propertyValue = typeConfig.OnDeserializing(instance, propertyName, propertyValue); } typeAccessor.SetProperty(instance, propertyValue); } @@ -172,9 +172,9 @@ internal static object StringToType( try { var propertyValue = typeAccessor.GetProperty(propertyValueStr); - if (typeConfig.OnDeserializingMember != null) + if (typeConfig.OnDeserializing != null) { - propertyValue = typeConfig.OnDeserializingMember(instance, propertyName, propertyValue); + propertyValue = typeConfig.OnDeserializing(instance, propertyName, propertyValue); } typeAccessor.SetProperty(instance, propertyValue); } @@ -184,10 +184,10 @@ internal static object StringToType( else Tracer.Instance.WriteWarning("WARN: failed to set property {0} with: {1}", propertyName, propertyValueStr); } } - else if (typeConfig.OnDeserializingMember != null) + else if (typeConfig.OnDeserializing != null) { // the property is not known by the DTO - typeConfig.OnDeserializingMember(instance, propertyName, propertyValueStr); + typeConfig.OnDeserializing(instance, propertyName, propertyValueStr); } //Serializer.EatItemSeperatorOrMapEndChar(strType, ref index); diff --git a/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs index b481f592b..d9481a148 100644 --- a/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs +++ b/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs @@ -98,9 +98,9 @@ internal static object StringToType( { var parseFn = Serializer.GetParseFn(propType); var propertyValue = parseFn(propertyValueStr); - if (typeConfig.OnDeserializingMember != null) + if (typeConfig.OnDeserializing != null) { - propertyValue = typeConfig.OnDeserializingMember(instance, propertyName, propertyValue); + propertyValue = typeConfig.OnDeserializing(instance, propertyName, propertyValue); } typeAccessor.SetProperty(instance, propertyValue); } @@ -122,9 +122,9 @@ internal static object StringToType( try { var propertyValue = typeAccessor.GetProperty(propertyValueStr); - if (typeConfig.OnDeserializingMember != null) + if (typeConfig.OnDeserializing != null) { - propertyValue = typeConfig.OnDeserializingMember(instance, propertyName, propertyValue); + propertyValue = typeConfig.OnDeserializing(instance, propertyName, propertyValue); } typeAccessor.SetProperty(instance, propertyValue); } @@ -134,10 +134,10 @@ internal static object StringToType( else Tracer.Instance.WriteWarning("WARN: failed to set property {0} with: {1}", propertyName, propertyValueStr); } } - else if (typeConfig.OnDeserializingMember != null) + else if (typeConfig.OnDeserializing != null) { // the property is not known by the DTO - typeConfig.OnDeserializingMember(instance, propertyName, propertyValueStr); + typeConfig.OnDeserializing(instance, propertyName, propertyValueStr); } //Serializer.EatItemSeperatorOrMapEndChar(strType, ref index); diff --git a/src/ServiceStack.Text/Common/WriteType.cs b/src/ServiceStack.Text/Common/WriteType.cs index fd7b5b5d9..2af57927c 100644 --- a/src/ServiceStack.Text/Common/WriteType.cs +++ b/src/ServiceStack.Text/Common/WriteType.cs @@ -109,9 +109,9 @@ static Func GetShouldSerializeMethod(MemberInfo member) return (method == null || method.ReturnType != typeof(bool)) ? null : (Func)Delegate.CreateDelegate(typeof(Func), method); } - static Func ShouldSerializeByName(Type type) + static Func ShouldSerialize(Type type) { - var method = type.GetMethod("ShouldSerializeByName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[] { typeof(string) }, null); + var method = type.GetMethod("ShouldSerialize", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[] { typeof(string) }, null); return (method == null || method.ReturnType != typeof(bool?)) ? null : (Func)Delegate.CreateDelegate(typeof(Func), method); @@ -134,7 +134,7 @@ private static bool Init() return typeof(T).IsDto(); } - var shouldSerializeByName = ShouldSerializeByName(typeof(T)); + var shouldSerializeByName = ShouldSerialize(typeof(T)); // NOTE: very limited support for DataContractSerialization (DCS) // NOT supporting Serializable diff --git a/src/ServiceStack.Text/JsConfig.cs b/src/ServiceStack.Text/JsConfig.cs index b409e3a78..da1583550 100644 --- a/src/ServiceStack.Text/JsConfig.cs +++ b/src/ServiceStack.Text/JsConfig.cs @@ -961,6 +961,18 @@ public static Func OnDeserializedFn set { onDeserializedFn = value; } } + public static bool HasDeserialingFn + { + get { return OnDeserializingFn != null; } + } + + private static Func onDeserializingFn; + public static Func OnDeserializingFn + { + get { return onDeserializingFn; } + set { onDeserializingFn = value; } + } + /// /// Exclude specific properties of this type from being serialized /// diff --git a/src/ServiceStack.Text/ReflectionExtensions.cs b/src/ServiceStack.Text/ReflectionExtensions.cs index 28c1263b4..98f644439 100644 --- a/src/ServiceStack.Text/ReflectionExtensions.cs +++ b/src/ServiceStack.Text/ReflectionExtensions.cs @@ -536,15 +536,14 @@ public static PropertyInfo[] GetSerializableProperties(this Type type) return publicReadableProperties.Where(prop => !prop.CustomAttributes(false).Any(attr => attr.GetType().Name == IgnoreDataMember)).ToArray(); } - public static Func GetOnDeserializingMember() + public static Func GetOnDeserializing() { - var method = typeof(T).GetMethod("OnDeserializingMember", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(string), typeof(object) }, null); + var method = typeof(T).GetMethod("OnDeserializing", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(string), typeof(object) }, null); if (method == null || method.ReturnType != typeof (object)) { return null; } - var ojb = (Func)Delegate.CreateDelegate(typeof(Func), method); - return (instance, memberName, value) => ojb((T)instance, memberName, value); + return (Func)Delegate.CreateDelegate(typeof(Func), method); } public static FieldInfo[] GetSerializableFields(this Type type) diff --git a/src/ServiceStack.Text/TypeConfig.cs b/src/ServiceStack.Text/TypeConfig.cs index 29392e767..8aff70ce4 100644 --- a/src/ServiceStack.Text/TypeConfig.cs +++ b/src/ServiceStack.Text/TypeConfig.cs @@ -10,7 +10,7 @@ internal class TypeConfig internal bool EnableAnonymousFieldSetterses; internal PropertyInfo[] Properties; internal FieldInfo[] Fields; - internal Func OnDeserializingMember; + internal Func OnDeserializing; internal TypeConfig(Type type) { @@ -23,7 +23,7 @@ internal TypeConfig(Type type) public static class TypeConfig { - private static readonly TypeConfig config; + internal static readonly TypeConfig config; public static PropertyInfo[] Properties { @@ -43,12 +43,6 @@ public static bool EnableAnonymousFieldSetters set { config.EnableAnonymousFieldSetterses = value; } } - public static Func OnDeserializingMember - { - get { return config.OnDeserializingMember; } - set { config.OnDeserializingMember = value; } - } - static TypeConfig() { config = new TypeConfig(typeof(T)); @@ -61,7 +55,14 @@ static TypeConfig() Properties = properties.Where(x => x.GetIndexParameters().Length == 0).ToArray(); Fields = config.Type.GetSerializableFields().ToArray(); - OnDeserializingMember = ReflectionExtensions.GetOnDeserializingMember(); + if (!JsConfig.HasDeserialingFn) + { + JsConfig.OnDeserializingFn = ReflectionExtensions.GetOnDeserializing(); + } + if (JsConfig.HasDeserialingFn) + { + config.OnDeserializing = (instance, memberName, value) => JsConfig.OnDeserializingFn((T) instance, memberName, value); + } } internal static TypeConfig GetState() diff --git a/tests/ServiceStack.Text.Tests/JsonTests/ConditionalSerializationTests.cs b/tests/ServiceStack.Text.Tests/JsonTests/ConditionalSerializationTests.cs index 1f630f337..57bc1b22b 100644 --- a/tests/ServiceStack.Text.Tests/JsonTests/ConditionalSerializationTests.cs +++ b/tests/ServiceStack.Text.Tests/JsonTests/ConditionalSerializationTests.cs @@ -128,7 +128,7 @@ public class ByNameFoo [DataMember(EmitDefaultValue = false, IsRequired = false)] public int? B { get; set; } - private bool? ShouldSerializeByName(string fieldName) + public bool? ShouldSerialize(string fieldName) { if (hasAttribute == null) { @@ -137,7 +137,7 @@ public class ByNameFoo return hasAttribute.Contains(fieldName); } - private object OnDeserializingMember(string fieldName, object value) + public object OnDeserializing(string fieldName, object value) { if (hasAttribute == null) { From 69fe47bd674e568090b0fae2bb43e410cbcc04e0 Mon Sep 17 00:00:00 2001 From: Brandon Cuff Date: Tue, 24 Feb 2015 16:15:42 -0800 Subject: [PATCH 04/17] Fixed IEnumerable's with mixed types. --- src/ServiceStack.Text/Common/WriteLists.cs | 8 +++-- .../EnumerableTests.cs | 30 +++++++++++++++++++ .../ServiceStack.Text.Tests.csproj | 1 + 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 tests/ServiceStack.Text.Tests/EnumerableTests.cs diff --git a/src/ServiceStack.Text/Common/WriteLists.cs b/src/ServiceStack.Text/Common/WriteLists.cs index a282b6590..1adbd712f 100644 --- a/src/ServiceStack.Text/Common/WriteLists.cs +++ b/src/ServiceStack.Text/Common/WriteLists.cs @@ -179,10 +179,14 @@ public static void WriteIEnumerable(TextWriter writer, object oValueCollection) var valueCollection = (IEnumerable)oValueCollection; var ranOnce = false; + Type lastType = null; foreach (var valueItem in valueCollection) { - if (toStringFn == null) - toStringFn = Serializer.GetWriteFn(valueItem.GetType()); + if (toStringFn == null || valueItem.GetType() != lastType) + { + lastType = valueItem.GetType(); + toStringFn = Serializer.GetWriteFn(lastType); + } JsWriter.WriteItemSeperatorIfRanOnce(writer, ref ranOnce); diff --git a/tests/ServiceStack.Text.Tests/EnumerableTests.cs b/tests/ServiceStack.Text.Tests/EnumerableTests.cs new file mode 100644 index 000000000..29dd109c5 --- /dev/null +++ b/tests/ServiceStack.Text.Tests/EnumerableTests.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; + +namespace ServiceStack.Text.Tests +{ + [TestFixture] + public class EnumerableTests + : TestBase + { + [Test] + public void Can_serialize_array_list_of_mixed_types() + { + var list = (IEnumerable)new ArrayList { + 1.0, + 1.1, + 1, + new object(), + "boo", + 1, + 1.2 + }; + + Serialize(list); + } + } +} diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index 6ee1b4611..6776438dd 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -164,6 +164,7 @@ + From b2e5144f82441b61772346a57ccce34c136ba19b Mon Sep 17 00:00:00 2001 From: Brandon Cuff Date: Wed, 25 Feb 2015 15:50:30 -0800 Subject: [PATCH 05/17] Fixed deserialization of null Dictionary. --- .../Common/DeserializeDictionary.cs | 4 ++ .../DictionaryTests.cs | 23 +++++++++++ .../ServiceStack.Text.Tests/HashtableTests.cs | 38 +++++++++++++++++++ .../ServiceStack.Text.Tests.csproj | 1 + 4 files changed, 66 insertions(+) create mode 100644 tests/ServiceStack.Text.Tests/HashtableTests.cs diff --git a/src/ServiceStack.Text/Common/DeserializeDictionary.cs b/src/ServiceStack.Text/Common/DeserializeDictionary.cs index afc453218..9cf716aa0 100644 --- a/src/ServiceStack.Text/Common/DeserializeDictionary.cs +++ b/src/ServiceStack.Text/Common/DeserializeDictionary.cs @@ -106,6 +106,8 @@ public static JsonObject ParseJsonObject(string value) #if !SILVERLIGHT public static Hashtable ParseHashtable(string value) { + if (value == null) return null; + var index = VerifyAndGetStartIndex(value, typeof(Hashtable)); var result = new Hashtable(); @@ -174,6 +176,8 @@ public static IDictionary ParseIDictionary(string value, Type dictType) public static Dictionary ParseStringDictionary(string value) { + if (value == null) return null; + var index = VerifyAndGetStartIndex(value, typeof(Dictionary)); var result = new Dictionary(); diff --git a/tests/ServiceStack.Text.Tests/DictionaryTests.cs b/tests/ServiceStack.Text.Tests/DictionaryTests.cs index bd7104d4e..e158a0d44 100644 --- a/tests/ServiceStack.Text.Tests/DictionaryTests.cs +++ b/tests/ServiceStack.Text.Tests/DictionaryTests.cs @@ -556,6 +556,29 @@ public void Can_deserialize_ordereddictionary_subclass() Assert.That(deserialized[2], Is.EqualTo("Value3")); Assert.That(deserialized["Key4"], Is.EqualTo(false)); } + + [Test] + public void Can_deserialize_null_string_string_dictionary() + { + using (var config = JsConfig.BeginScope()) + { + config.IncludeNullValues = true; + config.ThrowOnDeserializationError = true; + var original = new ModelWithDictionary { Value = null }; + var json = JsonSerializer.SerializeToString(original); + var deserialized = JsonSerializer.DeserializeFromString(json); + + json.Print(); + + Assert.That(deserialized, Is.Not.Null); + Assert.That(deserialized.Value, Is.EqualTo(original.Value)); + } + } + + private class ModelWithDictionary + { + public Dictionary Value { get; set; } + } } public class OrderedDictionarySub : OrderedDictionary { } diff --git a/tests/ServiceStack.Text.Tests/HashtableTests.cs b/tests/ServiceStack.Text.Tests/HashtableTests.cs new file mode 100644 index 000000000..22f93a87b --- /dev/null +++ b/tests/ServiceStack.Text.Tests/HashtableTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; + +namespace ServiceStack.Text.Tests +{ + [TestFixture] + public class HashtableTests + : TestBase + { + + [Test] + public void Can_deserialize_null_hashtable() + { + using (var config = JsConfig.BeginScope()) + { + config.IncludeNullValues = true; + config.ThrowOnDeserializationError = true; + var original = new ModelWithHashtable { Value = null }; + var json = JsonSerializer.SerializeToString(original); + var deserialized = JsonSerializer.DeserializeFromString(json); + + json.Print(); + + Assert.That(deserialized, Is.Not.Null); + Assert.That(deserialized.Value, Is.EqualTo(original.Value)); + } + } + + private class ModelWithHashtable + { + public Hashtable Value { get; set; } + } + } +} diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index 6776438dd..2dd54d949 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -165,6 +165,7 @@ + From b23f7a160fcb6441674ed7b8a9d989331e560e06 Mon Sep 17 00:00:00 2001 From: Brandon Cuff Date: Wed, 25 Feb 2015 17:12:05 -0800 Subject: [PATCH 06/17] Fixed null ref exception when deserializing - HashSet - HashSet --- .../Common/DeserializeCollection.cs | 4 ++ tests/ServiceStack.Text.Tests/HashSetTests.cs | 58 +++++++++++++++++++ .../ServiceStack.Text.Tests.csproj | 1 + 3 files changed, 63 insertions(+) create mode 100644 tests/ServiceStack.Text.Tests/HashSetTests.cs diff --git a/src/ServiceStack.Text/Common/DeserializeCollection.cs b/src/ServiceStack.Text/Common/DeserializeCollection.cs index ae63d932e..0f36b4790 100644 --- a/src/ServiceStack.Text/Common/DeserializeCollection.cs +++ b/src/ServiceStack.Text/Common/DeserializeCollection.cs @@ -54,12 +54,16 @@ public static ParseStringDelegate GetParseMethod(Type type) public static ICollection ParseStringCollection(string value, Type createType) { + if (value == null) return null; + var items = DeserializeArrayWithElements.ParseGenericArray(value, Serializer.ParseString); return CollectionExtensions.CreateAndPopulate(createType, items); } public static ICollection ParseIntCollection(string value, Type createType) { + if (value == null) return null; + var items = DeserializeArrayWithElements.ParseGenericArray(value, x => int.Parse(x)); return CollectionExtensions.CreateAndPopulate(createType, items); } diff --git a/tests/ServiceStack.Text.Tests/HashSetTests.cs b/tests/ServiceStack.Text.Tests/HashSetTests.cs new file mode 100644 index 000000000..2060c0ff9 --- /dev/null +++ b/tests/ServiceStack.Text.Tests/HashSetTests.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; + +namespace ServiceStack.Text.Tests +{ + [TestFixture] + public class HashSetTests + { + [Test] + public void Can_deserialize_null_string_collection() + { + using (var config = JsConfig.BeginScope()) + { + config.IncludeNullValues = true; + config.ThrowOnDeserializationError = true; + var original = new ModelWithStringHashSet { Value = null }; + var json = JsonSerializer.SerializeToString(original); + var deserialized = JsonSerializer.DeserializeFromString(json); + + json.Print(); + + Assert.That(deserialized, Is.Not.Null); + Assert.That(deserialized.Value, Is.EqualTo(original.Value)); + } + } + + [Test] + public void Can_deserialize_null_int_collection() + { + using (var config = JsConfig.BeginScope()) + { + config.IncludeNullValues = true; + config.ThrowOnDeserializationError = true; + var original = new ModelWithIntHashSet { Value = null }; + var json = JsonSerializer.SerializeToString(original); + var deserialized = JsonSerializer.DeserializeFromString(json); + + json.Print(); + + Assert.That(deserialized, Is.Not.Null); + Assert.That(deserialized.Value, Is.EqualTo(original.Value)); + } + } + + private class ModelWithStringHashSet + { + public HashSet Value { get; set; } + } + + private class ModelWithIntHashSet + { + public HashSet Value { get; set; } + } + } +} diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index 2dd54d949..f96c94d16 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -164,6 +164,7 @@ + From 4df1dd597dbcf798f3fa4e80ca7abb4258d0d555 Mon Sep 17 00:00:00 2001 From: Brandon Cuff Date: Wed, 25 Feb 2015 21:50:01 -0800 Subject: [PATCH 07/17] Fixed possible corruption of thread local state. When this manifests the serializer will begin writing things like this: "{\"foo\":123}" instead of this: {"foo":123} --- .../Common/WriteDictionary.cs | 81 +++++++++++++------ src/ServiceStack.Text/Common/WriteType.cs | 16 ++-- .../QueryStringSerializer.cs | 23 ++++-- .../DictionaryTests.cs | 27 +++++++ 4 files changed, 110 insertions(+), 37 deletions(-) diff --git a/src/ServiceStack.Text/Common/WriteDictionary.cs b/src/ServiceStack.Text/Common/WriteDictionary.cs index 852a5616c..9d2eaed89 100644 --- a/src/ServiceStack.Text/Common/WriteDictionary.cs +++ b/src/ServiceStack.Text/Common/WriteDictionary.cs @@ -123,22 +123,34 @@ public static void WriteIDictionary(TextWriter writer, object oMap) JsWriter.WriteItemSeperatorIfRanOnce(writer, ref ranOnce); JsState.WritingKeyCount++; - JsState.IsWritingValue = false; - - if (encodeMapKey) + try { - JsState.IsWritingValue = true; //prevent ""null"" - writer.Write(JsWriter.QuoteChar); - writeKeyFn(writer, key); - writer.Write(JsWriter.QuoteChar); + JsState.IsWritingValue = false; + + if (encodeMapKey) + { + JsState.IsWritingValue = true; //prevent ""null"" + try + { + writer.Write(JsWriter.QuoteChar); + writeKeyFn(writer, key); + writer.Write(JsWriter.QuoteChar); + } + finally + { + JsState.IsWritingValue = false; + } + } + else + { + writeKeyFn(writer, key); + } } - else + finally { - writeKeyFn(writer, key); + JsState.WritingKeyCount--; } - JsState.WritingKeyCount--; - writer.Write(JsWriter.MapKeySeperator); if (isNull) @@ -155,8 +167,14 @@ public static void WriteIDictionary(TextWriter writer, object oMap) } JsState.IsWritingValue = true; - writeValueFn(writer, dictionaryValue); - JsState.IsWritingValue = false; + try + { + writeValueFn(writer, dictionaryValue); + } + finally + { + JsState.IsWritingValue = false; + } } } @@ -203,22 +221,27 @@ public static void WriteGenericIDictionary( JsWriter.WriteItemSeperatorIfRanOnce(writer, ref ranOnce); JsState.WritingKeyCount++; - JsState.IsWritingValue = false; - - if (encodeMapKey) + try { - JsState.IsWritingValue = true; //prevent ""null"" - writer.Write(JsWriter.QuoteChar); - writeKeyFn(writer, kvp.Key); - writer.Write(JsWriter.QuoteChar); + JsState.IsWritingValue = false; + + if (encodeMapKey) + { + JsState.IsWritingValue = true; //prevent ""null"" + writer.Write(JsWriter.QuoteChar); + writeKeyFn(writer, kvp.Key); + writer.Write(JsWriter.QuoteChar); + } + else + { + writeKeyFn(writer, kvp.Key); + } } - else + finally { - writeKeyFn(writer, kvp.Key); + JsState.WritingKeyCount--; } - JsState.WritingKeyCount--; - writer.Write(JsWriter.MapKeySeperator); if (isNull) @@ -228,8 +251,14 @@ public static void WriteGenericIDictionary( else { JsState.IsWritingValue = true; - writeValueFn(writer, kvp.Value); - JsState.IsWritingValue = false; + try + { + writeValueFn(writer, kvp.Value); + } + finally + { + JsState.IsWritingValue = false; + } } } diff --git a/src/ServiceStack.Text/Common/WriteType.cs b/src/ServiceStack.Text/Common/WriteType.cs index 2af57927c..4c951244e 100644 --- a/src/ServiceStack.Text/Common/WriteType.cs +++ b/src/ServiceStack.Text/Common/WriteType.cs @@ -396,15 +396,21 @@ public static void WriteProperties(TextWriter writer, object value) writer.Write(JsWriter.MapKeySeperator); if (typeof(TSerializer) == typeof(JsonTypeSerializer)) JsState.IsWritingValue = true; - if (propertyValue == null) + try { - writer.Write(JsonUtils.Null); + if (propertyValue == null) + { + writer.Write(JsonUtils.Null); + } + else + { + propertyWriter.WriteFn(writer, propertyValue); + } } - else + finally { - propertyWriter.WriteFn(writer, propertyValue); + if (typeof(TSerializer) == typeof(JsonTypeSerializer)) JsState.IsWritingValue = false; } - if (typeof(TSerializer) == typeof(JsonTypeSerializer)) JsState.IsWritingValue = false; } } diff --git a/src/ServiceStack.Text/QueryStringSerializer.cs b/src/ServiceStack.Text/QueryStringSerializer.cs index 7cbecfb6e..cdef40fe9 100644 --- a/src/ServiceStack.Text/QueryStringSerializer.cs +++ b/src/ServiceStack.Text/QueryStringSerializer.cs @@ -168,17 +168,28 @@ public static void WriteIDictionary(TextWriter writer, object oMap) ranOnce = true; JsState.WritingKeyCount++; - JsState.IsWritingValue = false; - - writeKeyFn(writer, key); + try + { + JsState.IsWritingValue = false; - JsState.WritingKeyCount--; + writeKeyFn(writer, key); + } + finally + { + JsState.WritingKeyCount--; + } writer.Write("="); JsState.IsWritingValue = true; - writeValueFn(writer, dictionaryValue); - JsState.IsWritingValue = false; + try + { + writeValueFn(writer, dictionaryValue); + } + finally + { + JsState.IsWritingValue = false; + } } } finally diff --git a/tests/ServiceStack.Text.Tests/DictionaryTests.cs b/tests/ServiceStack.Text.Tests/DictionaryTests.cs index e158a0d44..f29cedd79 100644 --- a/tests/ServiceStack.Text.Tests/DictionaryTests.cs +++ b/tests/ServiceStack.Text.Tests/DictionaryTests.cs @@ -575,6 +575,33 @@ public void Can_deserialize_null_string_string_dictionary() } } + [Test] + public void Can_recover_from_exceptions_when_serializing_dictionary_keys() + { + var before = JsConfig.SerializeFn; + try + { + JsConfig.SerializeFn = v => + { + throw new Exception("Boom!"); + }; + var target = new Dictionary + { + { 1, "1" }, + }; + Assert.Throws(() => JsonSerializer.SerializeToString(target)); + } + finally + { + JsConfig.SerializeFn = before; + } + var json = JsonSerializer.SerializeToString(new ModelWithDictionary()); + + json.Print(); + + Assert.That(json.StartsWith("{"), Is.True); + } + private class ModelWithDictionary { public Dictionary Value { get; set; } From f39275c2bb779101d8680a1c1e400ceefd43905d Mon Sep 17 00:00:00 2001 From: voloda Date: Thu, 26 Mar 2015 15:27:42 +0100 Subject: [PATCH 08/17] Fix property filtering when a collection contains null --- src/ServiceStack.Text/Common/WriteType.cs | 11 +++-- .../JsonTests/JsonObjectTests.cs | 46 +++++++++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/src/ServiceStack.Text/Common/WriteType.cs b/src/ServiceStack.Text/Common/WriteType.cs index 4c951244e..b597cc9b0 100644 --- a/src/ServiceStack.Text/Common/WriteType.cs +++ b/src/ServiceStack.Text/Common/WriteType.cs @@ -348,13 +348,16 @@ public static void WriteProperties(TextWriter writer, object value) var len = PropertyWriters.Length; var exclude = JsConfig.ExcludePropertyReferences ?? new string[0]; ConvertToUpper(exclude); + + var hasValueNotNull = (value != null); + for (int index = 0; index < len; index++) { var propertyWriter = PropertyWriters[index]; - - if (propertyWriter.shouldSerialize != null && !propertyWriter.shouldSerialize((T)value)) continue; + + if (hasValueNotNull && propertyWriter.shouldSerialize != null && !propertyWriter.shouldSerialize((T)value)) continue; bool dontSkipDefault=false; - if (propertyWriter.shouldSerializeByName != null) + if (hasValueNotNull && propertyWriter.shouldSerializeByName != null) { var shouldSerialize = propertyWriter.shouldSerializeByName((T) value, propertyWriter.PropertyName); if (shouldSerialize.HasValue) @@ -369,7 +372,7 @@ public static void WriteProperties(TextWriter writer, object value) } } } - var propertyValue = value != null + var propertyValue = hasValueNotNull ? propertyWriter.GetterFn((T)value) : null; if (!dontSkipDefault) diff --git a/tests/ServiceStack.Text.Tests/JsonTests/JsonObjectTests.cs b/tests/ServiceStack.Text.Tests/JsonTests/JsonObjectTests.cs index 9c6089848..181f76415 100644 --- a/tests/ServiceStack.Text.Tests/JsonTests/JsonObjectTests.cs +++ b/tests/ServiceStack.Text.Tests/JsonTests/JsonObjectTests.cs @@ -234,6 +234,52 @@ public void Can_Serialize_NestedJsonValueDto() } } + [Test] + public void NullItemInCollectionShouldBeHandledGracefullyEvenIfShouldSerializeMethodExists() + { + var store = new ParentDto(); + store.ChildDtosWithShouldSerialize = new List() { new ChildDtoWithShouldSerialize() { Data = "xx" }, null }; + + var jj = JsonSerializer.SerializeToString(store); + Assert.AreEqual(jj, "{\"ChildDtosWithShouldSerialize\":[{\"Data\":\"xx\"},{}]}"); + } + + [Test] + public void NullItemInCollectionShouldBeHandledGracefullyEvenIfShouldSerializeForPropertyNameMethodExists() + { + var store = new ParentDto(); + store.ChildDtosWithShouldSerializeProperty = new List() { new ChildDtoWithShouldSerializeForProperty() { Data = "xx" }, null }; + + var jj = JsonSerializer.SerializeToString(store); + Assert.AreEqual(jj, "{\"ChildDtosWithShouldSerializeProperty\":[{\"Data\":\"xx\"},{}]}"); + } + + public class ParentDto + { + public IList ChildDtosWithShouldSerialize { get; set; } + public IList ChildDtosWithShouldSerializeProperty { get; set; } + } + + public class ChildDtoWithShouldSerialize + { + protected virtual bool? ShouldSerialize(string fieldName) + { + return true; + } + + public string Data { get; set; } + } + + public class ChildDtoWithShouldSerializeForProperty + { + public virtual bool ShouldSerializeData() + { + return true; + } + + public string Data { get; set; } + } + public class SimpleObj { public string value1 { get; set; } From 89d0282208b82d7912ab1943697951caa19d1fdb Mon Sep 17 00:00:00 2001 From: voloda Date: Fri, 27 Mar 2015 13:45:04 +0100 Subject: [PATCH 09/17] Fix null values serialization in ArrayList collection --- src/ServiceStack.Text/Common/WriteLists.cs | 19 ++++++++++-- .../EnumerableTests.cs | 31 +++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/ServiceStack.Text/Common/WriteLists.cs b/src/ServiceStack.Text/Common/WriteLists.cs index 1adbd712f..0699f2201 100644 --- a/src/ServiceStack.Text/Common/WriteLists.cs +++ b/src/ServiceStack.Text/Common/WriteLists.cs @@ -17,6 +17,7 @@ using System.Reflection; using System.Threading; using System.Linq; +using ServiceStack.Text.Json; namespace ServiceStack.Text.Common { @@ -182,10 +183,22 @@ public static void WriteIEnumerable(TextWriter writer, object oValueCollection) Type lastType = null; foreach (var valueItem in valueCollection) { - if (toStringFn == null || valueItem.GetType() != lastType) + if ((toStringFn == null) || (valueItem != null && valueItem.GetType() != lastType)) { - lastType = valueItem.GetType(); - toStringFn = Serializer.GetWriteFn(lastType); + if (valueItem != null) + { + if (valueItem.GetType() != lastType) + { + lastType = valueItem.GetType(); + toStringFn = Serializer.GetWriteFn(lastType); + } + } + else + { + // this can happen if the first item in the collection was null + lastType = typeof (object); + toStringFn = Serializer.GetWriteFn(lastType); + } } JsWriter.WriteItemSeperatorIfRanOnce(writer, ref ranOnce); diff --git a/tests/ServiceStack.Text.Tests/EnumerableTests.cs b/tests/ServiceStack.Text.Tests/EnumerableTests.cs index 29dd109c5..671ed7745 100644 --- a/tests/ServiceStack.Text.Tests/EnumerableTests.cs +++ b/tests/ServiceStack.Text.Tests/EnumerableTests.cs @@ -26,5 +26,36 @@ public void Can_serialize_array_list_of_mixed_types() Serialize(list); } + + [Test] + public void Can_serialize_array_list_of_mixed_types_with_null() + { + var list = (IEnumerable)new ArrayList { + 1.0, + null, + 1, + new object(), + "boo", + 1, + 1.2 + }; + + Serialize(list); + } + + [Test] + public void Can_serialize_array_list_of_mixed_types_with_null_on_first_position() + { + var list = (IEnumerable)new ArrayList { + null, + 1, + new object(), + "boo", + 1, + 1.2 + }; + + Serialize(list); + } } } From ff1102a51bd0e51b340a0a494fb60deb82e52980 Mon Sep 17 00:00:00 2001 From: voloda Date: Fri, 27 Mar 2015 15:57:57 +0100 Subject: [PATCH 10/17] Invoke error handler callback for deserialization issues --- .../Common/DeserializeTypeRefJson.cs | 2 + .../Common/DeserializeTypeRefJsv.cs | 2 + src/ServiceStack.Text/JsConfig.cs | 24 ++++ src/ServiceStack.Text/JsConfigScope.cs | 1 + .../OnDeserializationErrorCallbackTest.cs | 134 ++++++++++++++++++ .../JsonTests/ThrowOnDeserializeErrorTest.cs | 5 +- .../ServiceStack.Text.Tests.csproj | 1 + 7 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 tests/ServiceStack.Text.Tests/JsonTests/OnDeserializationErrorCallbackTest.cs diff --git a/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs index bff5c3cf6..eb92981d4 100644 --- a/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs +++ b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs @@ -162,6 +162,7 @@ internal static object StringToType( } catch (Exception e) { + if (JsConfig.HasOnDeserializationErrorHandler) JsConfig.OnDeserializationErrorCallback(instance, propertyName, propertyValueStr, propType, e); if (JsConfig.ThrowOnDeserializationError) throw DeserializeTypeRef.GetSerializationException(propertyName, propertyValueStr, propType, e); else Tracer.Instance.WriteWarning("WARN: failed to set dynamic property {0} with: {1}", propertyName, propertyValueStr); } @@ -180,6 +181,7 @@ internal static object StringToType( } catch (Exception e) { + if (JsConfig.HasOnDeserializationErrorHandler) JsConfig.OnDeserializationErrorCallback(instance, propertyName, propertyValueStr, typeAccessor.PropertyType, e); if (JsConfig.ThrowOnDeserializationError) throw DeserializeTypeRef.GetSerializationException(propertyName, propertyValueStr, typeAccessor.PropertyType, e); else Tracer.Instance.WriteWarning("WARN: failed to set property {0} with: {1}", propertyName, propertyValueStr); } diff --git a/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs index d9481a148..7a221f031 100644 --- a/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs +++ b/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs @@ -112,6 +112,7 @@ internal static object StringToType( } catch (Exception e) { + if (JsConfig.HasOnDeserializationErrorHandler) JsConfig.OnDeserializationErrorCallback(instance, propertyName, propertyValueStr, propType, e); if (JsConfig.ThrowOnDeserializationError) throw DeserializeTypeRef.GetSerializationException(propertyName, propertyValueStr, propType, e); else Tracer.Instance.WriteWarning("WARN: failed to set dynamic property {0} with: {1}", propertyName, propertyValueStr); } @@ -130,6 +131,7 @@ internal static object StringToType( } catch (Exception e) { + if (JsConfig.HasOnDeserializationErrorHandler) JsConfig.OnDeserializationErrorCallback(instance, propertyName, propertyValueStr, propType, e); if (JsConfig.ThrowOnDeserializationError) throw DeserializeTypeRef.GetSerializationException(propertyName, propertyValueStr, propType, e); else Tracer.Instance.WriteWarning("WARN: failed to set property {0} with: {1}", propertyName, propertyValueStr); } diff --git a/src/ServiceStack.Text/JsConfig.cs b/src/ServiceStack.Text/JsConfig.cs index da1583550..b88edd0f7 100644 --- a/src/ServiceStack.Text/JsConfig.cs +++ b/src/ServiceStack.Text/JsConfig.cs @@ -15,6 +15,8 @@ namespace ServiceStack.Text public static class JsConfig { + public delegate void Action(T1 p1, T2 p2, T3 p3, T4 p4, T5 p5); + static JsConfig() { //In-built default serialization, to Deserialize Color struct do: @@ -386,6 +388,27 @@ public static bool ThrowOnDeserializationError } } + /// + /// Gets or sets a value indicating if the framework should call an error handler when + /// an exception happens during the deserialization. + /// + /// Parameters have following meaning in order: deserialized entity, property name, parsed value, property type, caught exception. + private static Action sOnDeserializationErrorCallback; + public static Action OnDeserializationErrorCallback + { + get + { + return (JsConfigScope.Current != null ? JsConfigScope.Current.OnDeserializationErrorCallback : null) + ?? sOnDeserializationErrorCallback; + } + set { sOnDeserializationErrorCallback = value; } + } + + /// + /// Gets whether a deserialization error handler is configured or not. + /// + public static bool HasOnDeserializationErrorHandler { get { return OnDeserializationErrorCallback != null; } } + /// /// Gets or sets a value indicating if the framework should always convert to UTC format instead of local time. /// @@ -592,6 +615,7 @@ public static void Reset() TreatValueAsRefTypes = new HashSet { typeof(KeyValuePair<,>) }; PropertyConvention = JsonPropertyConvention.ExactMatch; sExcludePropertyReferences = null; + sOnDeserializationErrorCallback = null; } public static void Reset(Type cachesForType) diff --git a/src/ServiceStack.Text/JsConfigScope.cs b/src/ServiceStack.Text/JsConfigScope.cs index 7a9cca8f8..2e3b64cf8 100644 --- a/src/ServiceStack.Text/JsConfigScope.cs +++ b/src/ServiceStack.Text/JsConfigScope.cs @@ -81,5 +81,6 @@ public void Dispose() public int? MaxDepth { get; set; } public EmptyCtorFactoryDelegate ModelFactory { get; set; } public string[] ExcludePropertyReferences { get; set; } + public JsConfig.Action OnDeserializationErrorCallback { get; set; } } } diff --git a/tests/ServiceStack.Text.Tests/JsonTests/OnDeserializationErrorCallbackTest.cs b/tests/ServiceStack.Text.Tests/JsonTests/OnDeserializationErrorCallbackTest.cs new file mode 100644 index 000000000..4deaa2fb7 --- /dev/null +++ b/tests/ServiceStack.Text.Tests/JsonTests/OnDeserializationErrorCallbackTest.cs @@ -0,0 +1,134 @@ +using System; +using System.Runtime.Serialization; +using NUnit.Framework; + +namespace ServiceStack.Text.Tests.JsonTests +{ + [TestFixture] + public class OnDeserializationErrorCallbackTest + { + [Test] + public void Invokes_callback_on_protected_setter() + { + string json = @"{""idBadProt"":""value"", ""idGood"":""2"" }"; + + AssertThatInvalidJsonInvokesExpectedCallback(json, "idBadProt", "value", typeof(int), "Input string was not in a correct format."); + } + + [Test] + public void Invokes_callback_on_incorrect_type() + { + string json = @"{""idBad"":""abc"", ""idGood"":""2"" }"; + AssertThatInvalidJsonInvokesExpectedCallback(json, "idBad", "abc", typeof(int), "Input string was not in a correct format."); + } + + [Test] + public void Invokes_callback_on_incorrect_type_with_data_set() + { + string json = @"{""idBad"":""abc"", ""idGood"":""2"" }"; + AssertThatInvalidJsonInvokesExpectedCallback(json, "idBad", "abc", typeof(int), "Input string was not in a correct format."); + } + + [Test] + public void Invokes_callback_on_value_out_of_range() + { + string json = @"{""idBad"":""4700000007"", ""idGood"":""2"" }"; + AssertThatInvalidJsonInvokesExpectedCallback(json, "idBad", "4700000007", typeof(int), "Value was either too large or too small for an Int32."); + } + + [Test] + public void Does_not_invoke_callback_on_valid_data() + { + JsConfig.Reset(); + JsConfig.OnDeserializationErrorCallback = (o, s, s1, arg3, arg4) => Assert.Fail("For valida data this should not be invoked"); + + var json = @"{""idBad"":""2"", ""idGood"":""2"" }"; + JsonSerializer.DeserializeFromString(json, typeof(TestDto)); + } + + [Test] + public void TestReset() + { + JsConfig.Reset(); + Assert.IsNull(JsConfig.OnDeserializationErrorCallback); + JsConfig.OnDeserializationErrorCallback = (o, s, s1, arg3, arg4) => { }; + Assert.IsNotNull(JsConfig.OnDeserializationErrorCallback); + JsConfig.Reset(); + Assert.IsNull(JsConfig.OnDeserializationErrorCallback); + } + + [Test] + public void Invokes_callback_deserialization_of_array_with_missing_comma() + { + string json = @"{""Values"": [ { ""Val"": ""a""} { ""Val"": ""b""}] }"; + + AssertThatInvalidJsonInvokesExpectedCallback(json, "Values", @"[ { ""Val"": ""a""} { ""Val"": ""b""}]", typeof(TestChildDto[]), "Type definitions should start with a '{', expecting serialized type 'TestChildDto', got string starting with: Val"); + } + + [Test] + public void Does_not_invoke_callback_on_valid_data_with_array() + { + JsConfig.Reset(); + JsConfig.OnDeserializationErrorCallback = (o, s, s1, arg3, arg4) => Assert.Fail("For valida data this should not be invoked"); + + var json = @"{""Values"": [ { ""Val"": ""a""}, { ""Val"": ""b""}] }"; + JsonSerializer.DeserializeFromString(json, typeof(TestDtoWithArray)); + } + + [DataContract] + class TestDtoWithArray + { + [DataMember] + public TestChildDto[] Values { get; set; } + } + + [DataContract] + class TestChildDto + { + [DataMember] + public string Val { get; set; } + } + + [DataContract] + class TestDto + { + [DataMember(Name = "idBadProt")] + public int protId { get; protected set; } + [DataMember(Name = "idGood")] + public int IdGood { get; set; } + [DataMember(Name = "idBad")] + public int IdBad { get; set; } + } + + private static void AssertThatInvalidJsonInvokesExpectedCallback(string json, string expectedProperty, string expectedValue, Type expectedType, string expectedExceptionMessage) + { + string property = null, value = null; + Type type = null; + Exception ex = null; + bool callbackInvoked = false; + object deserialized = null; + + JsConfig.Reset(); + JsConfig.OnDeserializationErrorCallback = (o, s, s1, arg3, arg4) => + { + deserialized = o; + property = s; + value = s1; + type = arg3; + ex = arg4; + callbackInvoked = true; + }; + + + JsonSerializer.DeserializeFromString(json, typeof(T)); + + Assert.IsTrue(callbackInvoked, "Callback should be invoked"); + Assert.AreEqual(expectedProperty, property); + Assert.AreEqual(expectedValue, value); + Assert.AreEqual(expectedType, type); + Assert.AreEqual(expectedExceptionMessage, ex.Message); + Assert.IsNotNull(deserialized); + Assert.IsInstanceOf(deserialized); + } + } +} \ No newline at end of file diff --git a/tests/ServiceStack.Text.Tests/JsonTests/ThrowOnDeserializeErrorTest.cs b/tests/ServiceStack.Text.Tests/JsonTests/ThrowOnDeserializeErrorTest.cs index 8319658d0..59f96ccef 100644 --- a/tests/ServiceStack.Text.Tests/JsonTests/ThrowOnDeserializeErrorTest.cs +++ b/tests/ServiceStack.Text.Tests/JsonTests/ThrowOnDeserializeErrorTest.cs @@ -1,8 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Runtime.Serialization; +using System.Runtime.Serialization; using NUnit.Framework; -using ServiceStack.Text.Tests.Support; namespace ServiceStack.Text.Tests.JsonTests { diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index f96c94d16..a79f79020 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -167,6 +167,7 @@ + From 795027ee9a23ef4017f29a3b89b5a5816bfbfcfc Mon Sep 17 00:00:00 2001 From: voloda Date: Sat, 28 Mar 2015 21:34:05 +0100 Subject: [PATCH 11/17] Changes based on the feedback --- .../Common/DeserializeTypeRefJson.cs | 10 ++++++---- .../Common/DeserializeTypeRefJsv.cs | 14 +++++++------- src/ServiceStack.Text/Common/JsDelegates.cs | 2 ++ src/ServiceStack.Text/JsConfig.cs | 16 +++++++--------- src/ServiceStack.Text/JsConfigScope.cs | 3 ++- .../OnDeserializationErrorCallbackTest.cs | 18 +++++++++--------- 6 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs index eb92981d4..f26dbeafc 100644 --- a/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs +++ b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs @@ -162,9 +162,10 @@ internal static object StringToType( } catch (Exception e) { - if (JsConfig.HasOnDeserializationErrorHandler) JsConfig.OnDeserializationErrorCallback(instance, propertyName, propertyValueStr, propType, e); + if (JsConfig.HasOnDeserializationErrorHandler) JsConfig.OnDeserializationError(instance, propType, propertyName, propertyValueStr, e); if (JsConfig.ThrowOnDeserializationError) throw DeserializeTypeRef.GetSerializationException(propertyName, propertyValueStr, propType, e); - else Tracer.Instance.WriteWarning("WARN: failed to set dynamic property {0} with: {1}", propertyName, propertyValueStr); + + Tracer.Instance.WriteWarning("WARN: failed to set dynamic property {0} with: {1}", propertyName, propertyValueStr); } } @@ -181,9 +182,10 @@ internal static object StringToType( } catch (Exception e) { - if (JsConfig.HasOnDeserializationErrorHandler) JsConfig.OnDeserializationErrorCallback(instance, propertyName, propertyValueStr, typeAccessor.PropertyType, e); + if (JsConfig.HasOnDeserializationErrorHandler) JsConfig.OnDeserializationError(instance, typeAccessor.PropertyType, propertyName, propertyValueStr, e); if (JsConfig.ThrowOnDeserializationError) throw DeserializeTypeRef.GetSerializationException(propertyName, propertyValueStr, typeAccessor.PropertyType, e); - else Tracer.Instance.WriteWarning("WARN: failed to set property {0} with: {1}", propertyName, propertyValueStr); + + Tracer.Instance.WriteWarning("WARN: failed to set property {0} with: {1}", propertyName, propertyValueStr); } } else if (typeConfig.OnDeserializing != null) diff --git a/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs index 7a221f031..522727211 100644 --- a/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs +++ b/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Runtime.Serialization; -using System.Reflection; using ServiceStack.Text.Json; using ServiceStack.Text.Jsv; @@ -112,9 +110,10 @@ internal static object StringToType( } catch (Exception e) { - if (JsConfig.HasOnDeserializationErrorHandler) JsConfig.OnDeserializationErrorCallback(instance, propertyName, propertyValueStr, propType, e); + if (JsConfig.HasOnDeserializationErrorHandler) JsConfig.OnDeserializationError(instance, propType, propertyName, propertyValueStr, e); if (JsConfig.ThrowOnDeserializationError) throw DeserializeTypeRef.GetSerializationException(propertyName, propertyValueStr, propType, e); - else Tracer.Instance.WriteWarning("WARN: failed to set dynamic property {0} with: {1}", propertyName, propertyValueStr); + + Tracer.Instance.WriteWarning("WARN: failed to set dynamic property {0} with: {1}", propertyName, propertyValueStr); } } @@ -131,9 +130,10 @@ internal static object StringToType( } catch (Exception e) { - if (JsConfig.HasOnDeserializationErrorHandler) JsConfig.OnDeserializationErrorCallback(instance, propertyName, propertyValueStr, propType, e); - if (JsConfig.ThrowOnDeserializationError) throw DeserializeTypeRef.GetSerializationException(propertyName, propertyValueStr, propType, e); - else Tracer.Instance.WriteWarning("WARN: failed to set property {0} with: {1}", propertyName, propertyValueStr); + if (JsConfig.HasOnDeserializationErrorHandler) JsConfig.OnDeserializationError(instance, typeAccessor.PropertyType, propertyName, propertyValueStr, e); + if (JsConfig.ThrowOnDeserializationError) throw DeserializeTypeRef.GetSerializationException(propertyName, propertyValueStr, typeAccessor.PropertyType, e); + + Tracer.Instance.WriteWarning("WARN: failed to set property {0} with: {1}", propertyName, propertyValueStr); } } else if (typeConfig.OnDeserializing != null) diff --git a/src/ServiceStack.Text/Common/JsDelegates.cs b/src/ServiceStack.Text/Common/JsDelegates.cs index d5595eb83..e28a8c777 100644 --- a/src/ServiceStack.Text/Common/JsDelegates.cs +++ b/src/ServiceStack.Text/Common/JsDelegates.cs @@ -33,4 +33,6 @@ namespace ServiceStack.Text.Common public delegate object ConvertObjectDelegate(object fromObject); public delegate object ConvertInstanceDelegate(object obj, Type type); + + public delegate void DeserializationErrorDelegate(object instance, Type propertyType, string propertyName, string propertyValueStr, Exception e); } diff --git a/src/ServiceStack.Text/JsConfig.cs b/src/ServiceStack.Text/JsConfig.cs index b88edd0f7..051178430 100644 --- a/src/ServiceStack.Text/JsConfig.cs +++ b/src/ServiceStack.Text/JsConfig.cs @@ -15,8 +15,6 @@ namespace ServiceStack.Text public static class JsConfig { - public delegate void Action(T1 p1, T2 p2, T3 p3, T4 p4, T5 p5); - static JsConfig() { //In-built default serialization, to Deserialize Color struct do: @@ -393,21 +391,21 @@ public static bool ThrowOnDeserializationError /// an exception happens during the deserialization. /// /// Parameters have following meaning in order: deserialized entity, property name, parsed value, property type, caught exception. - private static Action sOnDeserializationErrorCallback; - public static Action OnDeserializationErrorCallback + private static DeserializationErrorDelegate sOnDeserializationError; + public static DeserializationErrorDelegate OnDeserializationError { get { - return (JsConfigScope.Current != null ? JsConfigScope.Current.OnDeserializationErrorCallback : null) - ?? sOnDeserializationErrorCallback; + return (JsConfigScope.Current != null ? JsConfigScope.Current.OnDeserializationError : null) + ?? sOnDeserializationError; } - set { sOnDeserializationErrorCallback = value; } + set { sOnDeserializationError = value; } } /// /// Gets whether a deserialization error handler is configured or not. /// - public static bool HasOnDeserializationErrorHandler { get { return OnDeserializationErrorCallback != null; } } + public static bool HasOnDeserializationErrorHandler { get { return OnDeserializationError != null; } } /// /// Gets or sets a value indicating if the framework should always convert to UTC format instead of local time. @@ -615,7 +613,7 @@ public static void Reset() TreatValueAsRefTypes = new HashSet { typeof(KeyValuePair<,>) }; PropertyConvention = JsonPropertyConvention.ExactMatch; sExcludePropertyReferences = null; - sOnDeserializationErrorCallback = null; + sOnDeserializationError = null; } public static void Reset(Type cachesForType) diff --git a/src/ServiceStack.Text/JsConfigScope.cs b/src/ServiceStack.Text/JsConfigScope.cs index 2e3b64cf8..c5657f74e 100644 --- a/src/ServiceStack.Text/JsConfigScope.cs +++ b/src/ServiceStack.Text/JsConfigScope.cs @@ -4,6 +4,7 @@ using System.Text; using System.Threading; using System.Diagnostics; +using ServiceStack.Text.Common; namespace ServiceStack.Text { @@ -81,6 +82,6 @@ public void Dispose() public int? MaxDepth { get; set; } public EmptyCtorFactoryDelegate ModelFactory { get; set; } public string[] ExcludePropertyReferences { get; set; } - public JsConfig.Action OnDeserializationErrorCallback { get; set; } + public DeserializationErrorDelegate OnDeserializationError { get; set; } } } diff --git a/tests/ServiceStack.Text.Tests/JsonTests/OnDeserializationErrorCallbackTest.cs b/tests/ServiceStack.Text.Tests/JsonTests/OnDeserializationErrorCallbackTest.cs index 4deaa2fb7..c183eaac0 100644 --- a/tests/ServiceStack.Text.Tests/JsonTests/OnDeserializationErrorCallbackTest.cs +++ b/tests/ServiceStack.Text.Tests/JsonTests/OnDeserializationErrorCallbackTest.cs @@ -5,7 +5,7 @@ namespace ServiceStack.Text.Tests.JsonTests { [TestFixture] - public class OnDeserializationErrorCallbackTest + public class OnDeserializationErrorTest { [Test] public void Invokes_callback_on_protected_setter() @@ -40,7 +40,7 @@ public void Invokes_callback_on_value_out_of_range() public void Does_not_invoke_callback_on_valid_data() { JsConfig.Reset(); - JsConfig.OnDeserializationErrorCallback = (o, s, s1, arg3, arg4) => Assert.Fail("For valida data this should not be invoked"); + JsConfig.OnDeserializationError = (o, s, s1, arg3, arg4) => Assert.Fail("For valida data this should not be invoked"); var json = @"{""idBad"":""2"", ""idGood"":""2"" }"; JsonSerializer.DeserializeFromString(json, typeof(TestDto)); @@ -50,11 +50,11 @@ public void Does_not_invoke_callback_on_valid_data() public void TestReset() { JsConfig.Reset(); - Assert.IsNull(JsConfig.OnDeserializationErrorCallback); - JsConfig.OnDeserializationErrorCallback = (o, s, s1, arg3, arg4) => { }; - Assert.IsNotNull(JsConfig.OnDeserializationErrorCallback); + Assert.IsNull(JsConfig.OnDeserializationError); + JsConfig.OnDeserializationError = (o, s, s1, arg3, arg4) => { }; + Assert.IsNotNull(JsConfig.OnDeserializationError); JsConfig.Reset(); - Assert.IsNull(JsConfig.OnDeserializationErrorCallback); + Assert.IsNull(JsConfig.OnDeserializationError); } [Test] @@ -69,7 +69,7 @@ public void Invokes_callback_deserialization_of_array_with_missing_comma() public void Does_not_invoke_callback_on_valid_data_with_array() { JsConfig.Reset(); - JsConfig.OnDeserializationErrorCallback = (o, s, s1, arg3, arg4) => Assert.Fail("For valida data this should not be invoked"); + JsConfig.OnDeserializationError = (o, s, s1, arg3, arg4) => Assert.Fail("For valida data this should not be invoked"); var json = @"{""Values"": [ { ""Val"": ""a""}, { ""Val"": ""b""}] }"; JsonSerializer.DeserializeFromString(json, typeof(TestDtoWithArray)); @@ -109,12 +109,12 @@ private static void AssertThatInvalidJsonInvokesExpectedCallback(string json, object deserialized = null; JsConfig.Reset(); - JsConfig.OnDeserializationErrorCallback = (o, s, s1, arg3, arg4) => + JsConfig.OnDeserializationError = (o, propertyType, s, s1, arg4) => { deserialized = o; property = s; value = s1; - type = arg3; + type = propertyType; ex = arg4; callbackInvoked = true; }; From f1a625b3d646baf40ca57c98bde851af1c93b344 Mon Sep 17 00:00:00 2001 From: xplicit Date: Wed, 8 Apr 2015 00:11:01 +0600 Subject: [PATCH 12/17] Fix number with leading zeros serialization bug JSON specification does not allow to use numbers with leading zeros see http://stackoverflow.com/questions/29048110/override-json-deserializing-a-number-with-a-leading-zero-as-a-decimal-and-not-an/29048193#29048193 for more information --- src/ServiceStack.Text/JsonObject.cs | 4 ++-- .../JsonTests/JsonObjectTests.cs | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/ServiceStack.Text/JsonObject.cs b/src/ServiceStack.Text/JsonObject.cs index 50e289098..8597ad0da 100644 --- a/src/ServiceStack.Text/JsonObject.cs +++ b/src/ServiceStack.Text/JsonObject.cs @@ -117,9 +117,9 @@ public string Child(string key) return base[key]; } #if !SILVERLIGHT && !MONOTOUCH - static readonly Regex NumberRegEx = new Regex(@"^[0-9]*(?:\.[0-9]*)?$", RegexOptions.Compiled); + static readonly Regex NumberRegEx = new Regex(@"^(0|[1-9]*)(?:\.[0-9]*)?$", RegexOptions.Compiled); #else - static readonly Regex NumberRegEx = new Regex(@"^[0-9]*(?:\.[0-9]*)?$"); + static readonly Regex NumberRegEx = new Regex(@"^(0|[1-9]*)(?:\.[0-9]*)?$"); #endif /// /// Write JSON Array, Object, bool or number values as raw string diff --git a/tests/ServiceStack.Text.Tests/JsonTests/JsonObjectTests.cs b/tests/ServiceStack.Text.Tests/JsonTests/JsonObjectTests.cs index 181f76415..a2cd72f19 100644 --- a/tests/ServiceStack.Text.Tests/JsonTests/JsonObjectTests.cs +++ b/tests/ServiceStack.Text.Tests/JsonTests/JsonObjectTests.cs @@ -26,6 +26,27 @@ public void Can_parse_empty_object_with_mixed_whitespaces() Assert.That(JsonObject.Parse("{ \n\t \n\r}"), Is.Empty); } + [Test] + public void Can_Serialize_numbers() + { + string notNumber = "{\"field\":\"00001\"}"; + Assert.AreEqual(notNumber, JsonObject.Parse(notNumber).ToJson()); + + string num1 = "{\"field\":0}"; + Assert.AreEqual(num1, JsonObject.Parse(num1).ToJson()); + + string num2 = "{\"field\":0.5}"; + Assert.AreEqual(num2, JsonObject.Parse(num2).ToJson()); + + string num3 = "{\"field\":.5}"; + Assert.AreEqual(num3, JsonObject.Parse(num3).ToJson()); + + string num4 = "{\"field\":12312}"; + Assert.AreEqual(num4, JsonObject.Parse(num4).ToJson()); + + string num5 = "{\"field\":12312.1231}"; + Assert.AreEqual(num5, JsonObject.Parse(num5).ToJson()); + } public class Jackalope { From 7385b168fac7dccf9ed3437d7b32a50cdf92edfc Mon Sep 17 00:00:00 2001 From: voloda Date: Thu, 16 Apr 2015 15:05:49 +0200 Subject: [PATCH 13/17] Explicitly specified __type should not be even activated when not appropriate --- .../Common/DeserializeTypeRefJson.cs | 37 +++++++++---------- .../Common/DeserializeTypeRefJsv.cs | 36 ++++++++---------- .../JsonTests/PolymorphicInstanceTest.cs | 22 +++++++++++ 3 files changed, 56 insertions(+), 39 deletions(-) diff --git a/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs index f26dbeafc..534050ae2 100644 --- a/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs +++ b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs @@ -91,37 +91,36 @@ internal static object StringToType( var explicitTypeName = Serializer.ParseString(propertyValueStr); var explicitType = AssemblyUtils.FindType(explicitTypeName); - if (explicitType != null && !explicitType.IsInterface() && !explicitType.IsAbstract()) + // let's do the type safety checks first before we even attempt to create + // a type instance + if (explicitType == null || explicitType.IsInterface() || explicitType.IsAbstract()) { - instance = explicitType.CreateInstance(); + Tracer.Instance.WriteWarning("Could not find type: " + propertyValueStr); } - - if (instance == null) + else if (!type.IsAssignableFrom(explicitType)) { - Tracer.Instance.WriteWarning("Could not find type: " + propertyValueStr); + Tracer.Instance.WriteWarning("Could not assign type: " + propertyValueStr); } else { - //If __type info doesn't match, ignore it. - if (!type.InstanceOfType(instance)) - { - instance = null; - } - else + instance = explicitType.CreateInstance(); + } + + if (instance != null) + { + var derivedType = instance.GetType(); + if (derivedType != type) { - var derivedType = instance.GetType(); - if (derivedType != type) + var derivedTypeConfig = new TypeConfig(derivedType); + var map = DeserializeTypeRef.GetTypeAccessorMap(derivedTypeConfig, Serializer); + if (map != null) { - var derivedTypeConfig = new TypeConfig(derivedType); - var map = DeserializeTypeRef.GetTypeAccessorMap(derivedTypeConfig, Serializer); - if (map != null) - { - typeAccessorMap = map; - } + typeAccessorMap = map; } } } + Serializer.EatItemSeperatorOrMapEndChar(strType, ref index); continue; } diff --git a/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs index 522727211..058434897 100644 --- a/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs +++ b/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs @@ -45,37 +45,33 @@ internal static object StringToType( var explicitTypeName = Serializer.ParseString(propertyValueStr); var explicitType = AssemblyUtils.FindType(explicitTypeName); - if (explicitType != null && !explicitType.IsInterface() && !explicitType.IsAbstract()) + if (explicitType == null || explicitType.IsInterface() || explicitType.IsAbstract()) { - instance = explicitType.CreateInstance(); + Tracer.Instance.WriteWarning("Could not find type: " + propertyValueStr); } - - if (instance == null) + else if (!type.IsAssignableFrom(explicitType)) { - Tracer.Instance.WriteWarning("Could not find type: " + propertyValueStr); + Tracer.Instance.WriteWarning("Could not assign type: " + propertyValueStr); } else { - //If __type info doesn't match, ignore it. - if (!type.InstanceOfType(instance)) - { - instance = null; - } - else + instance = explicitType.CreateInstance(); + } + + if (instance != null) + { + var derivedType = instance.GetType(); + if (derivedType != type) { - var derivedType = instance.GetType(); - if (derivedType != type) + var derivedTypeConfig = new TypeConfig(derivedType); + var map = DeserializeTypeRef.GetTypeAccessorMap(derivedTypeConfig, Serializer); + if (map != null) { - var derivedTypeConfig = new TypeConfig(derivedType); - var map = DeserializeTypeRef.GetTypeAccessorMap(derivedTypeConfig, Serializer); - if (map != null) - { - typeAccessorMap = map; - } + typeAccessorMap = map; } } } - + //Serializer.EatItemSeperatorOrMapEndChar(strType, ref index); if (index != strType.Length) index++; diff --git a/tests/ServiceStack.Text.Tests/JsonTests/PolymorphicInstanceTest.cs b/tests/ServiceStack.Text.Tests/JsonTests/PolymorphicInstanceTest.cs index d7b2a3ea1..ec479de50 100644 --- a/tests/ServiceStack.Text.Tests/JsonTests/PolymorphicInstanceTest.cs +++ b/tests/ServiceStack.Text.Tests/JsonTests/PolymorphicInstanceTest.cs @@ -44,5 +44,27 @@ public void Can_deserialise_polymorphic_list_exact_with_no_side_effect_for_bad_t } + [Test] + public void Should_not_even_instantiate_incorrect_type() + { + var json = @"{""__type"":""" + + typeof(TestClass).ToTypeString() + @""", ""Name"":""Fido""}"; + var dog = JsonSerializer.DeserializeFromString( + + json); + + Assert.IsFalse(TestClass.Called); + } + + public class TestClass + { + public static bool Called { get; set; } + + public TestClass() + { + Called = true; + } + } + } } From 2ebe97b0a639f6f7f74256c588d11649264b6ecd Mon Sep 17 00:00:00 2001 From: Sergey Bogdanov Date: Tue, 21 Apr 2015 14:29:37 +0300 Subject: [PATCH 14/17] - converted to Unified --- .../ServiceStack.Text.MonoTouch.csproj | 10 ++++++---- src/ServiceStack.Text/JsConfig.cs | 14 +++++++------- src/ServiceStack.Text/Marc/TypeAccessor.cs | 4 +++- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/ServiceStack.Text.MonoTouch/ServiceStack.Text.MonoTouch/ServiceStack.Text.MonoTouch.csproj b/src/ServiceStack.Text.MonoTouch/ServiceStack.Text.MonoTouch/ServiceStack.Text.MonoTouch.csproj index 651fdcf5f..d25c38576 100644 --- a/src/ServiceStack.Text.MonoTouch/ServiceStack.Text.MonoTouch/ServiceStack.Text.MonoTouch.csproj +++ b/src/ServiceStack.Text.MonoTouch/ServiceStack.Text.MonoTouch/ServiceStack.Text.MonoTouch.csproj @@ -3,13 +3,15 @@ Debug AnyCPU - 10.0.0 + 8.0.30703 2.0 {1137B5AC-2259-413C-A473-93721D2A7551} - {6BC8ED88-2882-458C-8E55-DFD12B67127B};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} Library ServiceStack.Text.MonoTouch ServiceStack.Text.MonoTouch + Xamarin.iOS + v1.0 True @@ -34,10 +36,9 @@ - + - AssemblyUtils.cs @@ -279,4 +280,5 @@ ServiceStack.Text.suo + \ No newline at end of file diff --git a/src/ServiceStack.Text/JsConfig.cs b/src/ServiceStack.Text/JsConfig.cs index 051178430..11175a4a7 100644 --- a/src/ServiceStack.Text/JsConfig.cs +++ b/src/ServiceStack.Text/JsConfig.cs @@ -637,11 +637,11 @@ internal static void InvokeReset(this Type genericType) /// Provide hint to MonoTouch AOT compiler to pre-compile generic classes for all your DTOs. /// Just needs to be called once in a static constructor. /// - [MonoTouch.Foundation.Preserve] + [Foundation.Preserve] public static void InitForAot() { } - [MonoTouch.Foundation.Preserve] + [Foundation.Preserve] public static void RegisterForAot() { RegisterTypeForAot(); @@ -690,20 +690,20 @@ public static void RegisterForAot() RegisterTypeForAot(); } - [MonoTouch.Foundation.Preserve] + [Foundation.Preserve] public static void RegisterTypeForAot() { AotConfig.RegisterSerializers(); } - [MonoTouch.Foundation.Preserve] + [Foundation.Preserve] static void RegisterQueryStringWriter() { var i = 0; if (QueryStringWriter.WriteFn() != null) i++; } - [MonoTouch.Foundation.Preserve] + [Foundation.Preserve] internal static int RegisterElement() { var i = 0; @@ -716,7 +716,7 @@ internal static int RegisterElement() /// /// Class contains Ahead-of-Time (AOT) explicit class declarations which is used only to workaround "-aot-only" exceptions occured on device only. /// - [MonoTouch.Foundation.Preserve(AllMembers=true)] + [Foundation.Preserve(AllMembers=true)] internal class AotConfig { internal static JsReader jsonReader; @@ -851,7 +851,7 @@ internal static void RegisterElement() where TSerializ } #if MONOTOUCH - [MonoTouch.Foundation.Preserve(AllMembers=true)] + [Foundation.Preserve(AllMembers=true)] internal class Poco { public string Dummy { get; set; } diff --git a/src/ServiceStack.Text/Marc/TypeAccessor.cs b/src/ServiceStack.Text/Marc/TypeAccessor.cs index 403ee3920..a5922ca39 100644 --- a/src/ServiceStack.Text/Marc/TypeAccessor.cs +++ b/src/ServiceStack.Text/Marc/TypeAccessor.cs @@ -1,7 +1,9 @@ -using System; +using System; using System.Collections; using System.Reflection; +#if !SILVERLIGHT && !MONOTOUCH && !XBOX using System.Reflection.Emit; +#endif using System.Threading; //using System.Dynamic; From 66b1ff43b80003c4ceb8e55f8cd7f80377143810 Mon Sep 17 00:00:00 2001 From: Maxim Mass Date: Mon, 25 May 2015 23:54:04 -0700 Subject: [PATCH 15/17] Allowing deserialization of object arrays when some of the objects are null but the first one is not null --- .../Common/DeserializeListWithElements.cs | 9 ++++++- .../JsonTests/JsonArrayObjectTests.cs | 25 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/ServiceStack.Text/Common/DeserializeListWithElements.cs b/src/ServiceStack.Text/Common/DeserializeListWithElements.cs index 41d1b9677..8292d943d 100644 --- a/src/ServiceStack.Text/Common/DeserializeListWithElements.cs +++ b/src/ServiceStack.Text/Common/DeserializeListWithElements.cs @@ -141,7 +141,14 @@ public static ICollection ParseGenericList(string value, Type createListType, do { var itemValue = Serializer.EatTypeValue(value, ref i); - to.Add((T)parseFn(itemValue)); + if (itemValue != null) + { + to.Add((T)parseFn(itemValue)); + } + else + { + to.Add(default(T)); + } Serializer.EatWhitespace(value, ref i); } while (++i < value.Length); } diff --git a/tests/ServiceStack.Text.Tests/JsonTests/JsonArrayObjectTests.cs b/tests/ServiceStack.Text.Tests/JsonTests/JsonArrayObjectTests.cs index 4619556d8..18d06c880 100644 --- a/tests/ServiceStack.Text.Tests/JsonTests/JsonArrayObjectTests.cs +++ b/tests/ServiceStack.Text.Tests/JsonTests/JsonArrayObjectTests.cs @@ -69,5 +69,30 @@ public void Can_parse_empty_array_in_dto_with_tab() var oPretty = prettyJson.FromJson(); Assert.That(oPretty.Names.Count, Is.EqualTo(0)); } + + public class MyClass + { + public string Item { get; set; } + } + + [Test] + public void Can_parse_array_with_null_objects_starting_with_not_null_item() + { + var compactJson = @"{""items"":[{""Item"":""myitem""},null]}"; + var json = JsonObject.Parse(compactJson); + var items = json.ArrayObjects("items"); + Assert.NotNull(items[0]); + Assert.Null(items[1]); + } + + [Test] + public void Can_parse_array_with_null_objects_starting_with_null_item() + { + var compactJson = @"{""items"":[null,{""Item"":""myitem""}]}"; + var json = JsonObject.Parse(compactJson); + var items = json.ArrayObjects("items"); + Assert.Null(items[0]); + Assert.NotNull(items[1]); + } } } From e8e1aa11b28cb21d552d0377c404fd9f60edbc71 Mon Sep 17 00:00:00 2001 From: voloda Date: Fri, 5 Jun 2015 11:13:10 +0200 Subject: [PATCH 16/17] Dictionary should not skip null values even if null values should not be serialized --- src/ServiceStack.Text/Common/ITypeSerializer.cs | 2 ++ src/ServiceStack.Text/Common/WriteDictionary.cs | 4 ++-- src/ServiceStack.Text/JsConfig.cs | 15 +++++++++++++++ src/ServiceStack.Text/JsConfigScope.cs | 1 + src/ServiceStack.Text/Json/JsonTypeSerializer.cs | 5 +++++ src/ServiceStack.Text/Jsv/JsvTypeSerializer.cs | 5 +++++ tests/ServiceStack.Text.Tests/DictionaryTests.cs | 8 ++++++-- 7 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/ServiceStack.Text/Common/ITypeSerializer.cs b/src/ServiceStack.Text/Common/ITypeSerializer.cs index f85e311d5..e8fda4c7a 100644 --- a/src/ServiceStack.Text/Common/ITypeSerializer.cs +++ b/src/ServiceStack.Text/Common/ITypeSerializer.cs @@ -7,6 +7,8 @@ namespace ServiceStack.Text.Common internal interface ITypeSerializer { bool IncludeNullValues { get; } + bool IncludeNullValuesInDictionaries { get; } + string TypeAttrInObject { get; } WriteObjectDelegate GetWriteFn(); diff --git a/src/ServiceStack.Text/Common/WriteDictionary.cs b/src/ServiceStack.Text/Common/WriteDictionary.cs index 9d2eaed89..4e8ed5283 100644 --- a/src/ServiceStack.Text/Common/WriteDictionary.cs +++ b/src/ServiceStack.Text/Common/WriteDictionary.cs @@ -110,7 +110,7 @@ public static void WriteIDictionary(TextWriter writer, object oMap) var dictionaryValue = map[key]; var isNull = (dictionaryValue == null); - if (isNull && !Serializer.IncludeNullValues) continue; + if (isNull && !Serializer.IncludeNullValuesInDictionaries) continue; var keyType = key.GetType(); if (writeKeyFn == null || lastKeyType != keyType) @@ -216,7 +216,7 @@ public static void WriteGenericIDictionary( foreach (var kvp in map) { var isNull = (kvp.Value == null); - if (isNull && !Serializer.IncludeNullValues) continue; + if (isNull && !Serializer.IncludeNullValuesInDictionaries) continue; JsWriter.WriteItemSeperatorIfRanOnce(writer, ref ranOnce); diff --git a/src/ServiceStack.Text/JsConfig.cs b/src/ServiceStack.Text/JsConfig.cs index 11175a4a7..da04f1541 100644 --- a/src/ServiceStack.Text/JsConfig.cs +++ b/src/ServiceStack.Text/JsConfig.cs @@ -142,6 +142,21 @@ public static bool IncludeNullValues } } + private static bool? sIncludeNullValuesInDictionaries; + public static bool IncludeNullValuesInDictionaries + { + get + { + return (JsConfigScope.Current != null ? JsConfigScope.Current.IncludeNullValuesInDictionaries : null) + ?? sIncludeNullValuesInDictionaries + ?? false; + } + set + { + if (!sIncludeNullValuesInDictionaries.HasValue) sIncludeNullValues = value; + } + } + private static bool? sTreatEnumAsInteger; public static bool TreatEnumAsInteger { diff --git a/src/ServiceStack.Text/JsConfigScope.cs b/src/ServiceStack.Text/JsConfigScope.cs index c5657f74e..45362217e 100644 --- a/src/ServiceStack.Text/JsConfigScope.cs +++ b/src/ServiceStack.Text/JsConfigScope.cs @@ -60,6 +60,7 @@ public void Dispose() public bool? TryToParsePrimitiveTypeValues { get; set; } public bool? TryToParseNumericType { get; set; } public bool? IncludeNullValues { get; set; } + public bool? IncludeNullValuesInDictionaries { get; set; } public bool? TreatEnumAsInteger { get; set; } public bool? ExcludeTypeInfo { get; set; } public bool? IncludeTypeInfo { get; set; } diff --git a/src/ServiceStack.Text/Json/JsonTypeSerializer.cs b/src/ServiceStack.Text/Json/JsonTypeSerializer.cs index 732f5cb76..c73613624 100644 --- a/src/ServiceStack.Text/Json/JsonTypeSerializer.cs +++ b/src/ServiceStack.Text/Json/JsonTypeSerializer.cs @@ -28,6 +28,11 @@ public bool IncludeNullValues get { return JsConfig.IncludeNullValues; } } + public bool IncludeNullValuesInDictionaries + { + get { return JsConfig.IncludeNullValues; } + } + public string TypeAttrInObject { get { return JsConfig.JsonTypeAttrInObject; } diff --git a/src/ServiceStack.Text/Jsv/JsvTypeSerializer.cs b/src/ServiceStack.Text/Jsv/JsvTypeSerializer.cs index 71ef18e7e..54389fd97 100644 --- a/src/ServiceStack.Text/Jsv/JsvTypeSerializer.cs +++ b/src/ServiceStack.Text/Jsv/JsvTypeSerializer.cs @@ -28,6 +28,11 @@ public bool IncludeNullValues get { return false; } //Doesn't support null values, treated as "null" string literal } + public bool IncludeNullValuesInDictionaries + { + get { return false; } //Doesn't support null values, treated as "null" string literal + } + public string TypeAttrInObject { get { return JsConfig.JsvTypeAttrInObject; } diff --git a/tests/ServiceStack.Text.Tests/DictionaryTests.cs b/tests/ServiceStack.Text.Tests/DictionaryTests.cs index f29cedd79..6e6b944dd 100644 --- a/tests/ServiceStack.Text.Tests/DictionaryTests.cs +++ b/tests/ServiceStack.Text.Tests/DictionaryTests.cs @@ -376,7 +376,9 @@ public void Can_deserialize_mixed_dictionary_into_strongtyped_map() [Test] public void Can_serialise_null_values_from_dictionary_correctly() { - JsConfig.IncludeNullValues = true; + JsConfig.IncludeNullValues = false; + JsConfig.IncludeNullValuesInDictionaries = true; + var dictionary = new Dictionary { { "value", null } }; Serialize(dictionary, includeXml: false); @@ -391,7 +393,9 @@ public void Can_serialise_null_values_from_dictionary_correctly() [Test] public void Will_ignore_null_values_from_dictionary_correctly() { - JsConfig.IncludeNullValues = false; + JsConfig.IncludeNullValues = true; + JsConfig.IncludeNullValuesInDictionaries = false; + var dictionary = new Dictionary { { "value", null } }; Serialize(dictionary, includeXml: false); From df792418c1246a009540136da2a98f6e5d279130 Mon Sep 17 00:00:00 2001 From: voloda Date: Mon, 8 Jun 2015 09:26:04 +0200 Subject: [PATCH 17/17] Fix copy&paste issues --- src/ServiceStack.Text/JsConfig.cs | 3 ++- src/ServiceStack.Text/Json/JsonTypeSerializer.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ServiceStack.Text/JsConfig.cs b/src/ServiceStack.Text/JsConfig.cs index da04f1541..9b548f43b 100644 --- a/src/ServiceStack.Text/JsConfig.cs +++ b/src/ServiceStack.Text/JsConfig.cs @@ -153,7 +153,7 @@ public static bool IncludeNullValuesInDictionaries } set { - if (!sIncludeNullValuesInDictionaries.HasValue) sIncludeNullValues = value; + if (!sIncludeNullValuesInDictionaries.HasValue) sIncludeNullValuesInDictionaries = value; } } @@ -606,6 +606,7 @@ public static void Reset() sTryToParseNumericType = null; sConvertObjectTypesIntoStringDictionary = null; sIncludeNullValues = null; + sIncludeNullValuesInDictionaries = null; sExcludeTypeInfo = null; sEmitCamelCaseNames = null; sEmitLowercaseUnderscoreNames = null; diff --git a/src/ServiceStack.Text/Json/JsonTypeSerializer.cs b/src/ServiceStack.Text/Json/JsonTypeSerializer.cs index c73613624..b447ccf00 100644 --- a/src/ServiceStack.Text/Json/JsonTypeSerializer.cs +++ b/src/ServiceStack.Text/Json/JsonTypeSerializer.cs @@ -30,7 +30,7 @@ public bool IncludeNullValues public bool IncludeNullValuesInDictionaries { - get { return JsConfig.IncludeNullValues; } + get { return JsConfig.IncludeNullValuesInDictionaries; } } public string TypeAttrInObject