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/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/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/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/src/ServiceStack.Text/Common/DeserializeType.cs b/src/ServiceStack.Text/Common/DeserializeType.cs index e23b25c61..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) @@ -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/Common/DeserializeTypeRefJson.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs index 3068db8e3..534050ae2 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; @@ -90,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; } @@ -140,6 +140,10 @@ internal static object StringToType( var parseFn = JsonReader.GetParseFn(propType); var propertyValue = parseFn(propertyValueStr); + if (typeConfig.OnDeserializing != null) + { + propertyValue = typeConfig.OnDeserializing(instance, propertyName, propertyValue); + } typeAccessor.SetProperty(instance, propertyValue); } @@ -157,8 +161,10 @@ internal static object StringToType( } catch (Exception 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); } } @@ -167,14 +173,25 @@ internal static object StringToType( try { var propertyValue = typeAccessor.GetProperty(propertyValueStr); + if (typeConfig.OnDeserializing != null) + { + propertyValue = typeConfig.OnDeserializing(instance, propertyName, propertyValue); + } typeAccessor.SetProperty(instance, propertyValue); } catch (Exception 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) + { + // the property is not known by the DTO + typeConfig.OnDeserializing(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..058434897 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; @@ -12,12 +10,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; @@ -46,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++; @@ -97,6 +92,10 @@ internal static object StringToType( { var parseFn = Serializer.GetParseFn(propType); var propertyValue = parseFn(propertyValueStr); + if (typeConfig.OnDeserializing != null) + { + propertyValue = typeConfig.OnDeserializing(instance, propertyName, propertyValue); + } typeAccessor.SetProperty(instance, propertyValue); } @@ -107,8 +106,10 @@ internal static object StringToType( } catch (Exception 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); } } @@ -117,14 +118,25 @@ internal static object StringToType( try { var propertyValue = typeAccessor.GetProperty(propertyValueStr); + if (typeConfig.OnDeserializing != null) + { + propertyValue = typeConfig.OnDeserializing(instance, propertyName, propertyValue); + } typeAccessor.SetProperty(instance, propertyValue); } catch (Exception 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) + { + // the property is not known by the DTO + typeConfig.OnDeserializing(instance, propertyName, propertyValueStr); + } //Serializer.EatItemSeperatorOrMapEndChar(strType, ref index); if (index != strType.Length) index++; 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/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/Common/WriteDictionary.cs b/src/ServiceStack.Text/Common/WriteDictionary.cs index 852a5616c..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) @@ -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; + } } } @@ -198,27 +216,32 @@ 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); 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/WriteLists.cs b/src/ServiceStack.Text/Common/WriteLists.cs index a282b6590..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 { @@ -179,10 +180,26 @@ 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 != null && valueItem.GetType() != 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/src/ServiceStack.Text/Common/WriteType.cs b/src/ServiceStack.Text/Common/WriteType.cs index 721e08d54..b597cc9b0 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 ShouldSerialize(Type type) + { + 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); + } + + 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 = ShouldSerialize(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; } } @@ -332,26 +348,46 @@ 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; - - var propertyValue = value != null - ? propertyWriter.GetterFn((T)value) - : null; - if (propertyWriter.propertySuppressDefaultAttribute && Equals(propertyWriter.DefaultValue, propertyValue)) + if (hasValueNotNull && propertyWriter.shouldSerialize != null && !propertyWriter.shouldSerialize((T)value)) continue; + bool dontSkipDefault=false; + if (hasValueNotNull && propertyWriter.shouldSerializeByName != null) { - continue; + var shouldSerialize = propertyWriter.shouldSerializeByName((T) value, propertyWriter.PropertyName); + if (shouldSerialize.HasValue) + { + if (shouldSerialize.Value) + { + dontSkipDefault = true; + } + else + { + continue; + } + } } - if ((propertyValue == null - || (propertyWriter.propertySuppressDefaultConfig && Equals(propertyWriter.DefaultValue, propertyValue))) - && !Serializer.IncludeNullValues - ) + var propertyValue = hasValueNotNull + ? propertyWriter.GetterFn((T)value) + : null; + 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; @@ -363,15 +399,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/JsConfig.cs b/src/ServiceStack.Text/JsConfig.cs index b409e3a78..9b548f43b 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) sIncludeNullValuesInDictionaries = value; + } + } + private static bool? sTreatEnumAsInteger; public static bool TreatEnumAsInteger { @@ -386,6 +401,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 DeserializationErrorDelegate sOnDeserializationError; + public static DeserializationErrorDelegate OnDeserializationError + { + get + { + return (JsConfigScope.Current != null ? JsConfigScope.Current.OnDeserializationError : null) + ?? sOnDeserializationError; + } + set { sOnDeserializationError = value; } + } + + /// + /// Gets whether a deserialization error handler is configured or not. + /// + 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. /// @@ -570,6 +606,7 @@ public static void Reset() sTryToParseNumericType = null; sConvertObjectTypesIntoStringDictionary = null; sIncludeNullValues = null; + sIncludeNullValuesInDictionaries = null; sExcludeTypeInfo = null; sEmitCamelCaseNames = null; sEmitLowercaseUnderscoreNames = null; @@ -592,6 +629,7 @@ public static void Reset() TreatValueAsRefTypes = new HashSet { typeof(KeyValuePair<,>) }; PropertyConvention = JsonPropertyConvention.ExactMatch; sExcludePropertyReferences = null; + sOnDeserializationError = null; } public static void Reset(Type cachesForType) @@ -615,11 +653,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(); @@ -668,20 +706,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; @@ -694,7 +732,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; @@ -829,7 +867,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; } @@ -961,6 +999,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/JsConfigScope.cs b/src/ServiceStack.Text/JsConfigScope.cs index 7a9cca8f8..45362217e 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 { @@ -59,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; } @@ -81,5 +83,6 @@ public void Dispose() public int? MaxDepth { get; set; } public EmptyCtorFactoryDelegate ModelFactory { get; set; } public string[] ExcludePropertyReferences { get; set; } + public DeserializationErrorDelegate OnDeserializationError { get; set; } } } diff --git a/src/ServiceStack.Text/Json/JsonTypeSerializer.cs b/src/ServiceStack.Text/Json/JsonTypeSerializer.cs index fb3809713..b447ccf00 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.IncludeNullValuesInDictionaries; } + } + public string TypeAttrInObject { get { return JsConfig.JsonTypeAttrInObject; } @@ -42,10 +47,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/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/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/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; 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/src/ServiceStack.Text/ReflectionExtensions.cs b/src/ServiceStack.Text/ReflectionExtensions.cs index 95ed990f6..98f644439 100644 --- a/src/ServiceStack.Text/ReflectionExtensions.cs +++ b/src/ServiceStack.Text/ReflectionExtensions.cs @@ -536,6 +536,16 @@ public static PropertyInfo[] GetSerializableProperties(this Type type) return publicReadableProperties.Where(prop => !prop.CustomAttributes(false).Any(attr => attr.GetType().Name == IgnoreDataMember)).ToArray(); } + public static Func GetOnDeserializing() + { + 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; + } + return (Func)Delegate.CreateDelegate(typeof(Func), method); + } + 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..8aff70ce4 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 OnDeserializing; internal TypeConfig(Type type) { @@ -22,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 { @@ -54,6 +55,14 @@ static TypeConfig() Properties = properties.Where(x => x.GetIndexParameters().Length == 0).ToArray(); Fields = config.Type.GetSerializableFields().ToArray(); + 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/DictionaryTests.cs b/tests/ServiceStack.Text.Tests/DictionaryTests.cs index bd7104d4e..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); @@ -556,6 +560,56 @@ 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)); + } + } + + [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; } + } } public class OrderedDictionarySub : OrderedDictionary { } diff --git a/tests/ServiceStack.Text.Tests/EnumerableTests.cs b/tests/ServiceStack.Text.Tests/EnumerableTests.cs new file mode 100644 index 000000000..671ed7745 --- /dev/null +++ b/tests/ServiceStack.Text.Tests/EnumerableTests.cs @@ -0,0 +1,61 @@ +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); + } + + [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); + } + } +} 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/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/JsonTests/ConditionalSerializationTests.cs b/tests/ServiceStack.Text.Tests/JsonTests/ConditionalSerializationTests.cs index f2eb1d92d..57bc1b22b 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; } + + public bool? ShouldSerialize(string fieldName) + { + if (hasAttribute == null) + { + return null; + } + return hasAttribute.Contains(fieldName); + } + + public object OnDeserializing(string fieldName, object value) + { + if (hasAttribute == null) + { + hasAttribute=new HashSet(); + } + hasAttribute.Add(fieldName); + return value; + } + } } } 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]); + } } } diff --git a/tests/ServiceStack.Text.Tests/JsonTests/JsonObjectTests.cs b/tests/ServiceStack.Text.Tests/JsonTests/JsonObjectTests.cs index 9c6089848..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 { @@ -234,6 +255,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; } diff --git a/tests/ServiceStack.Text.Tests/JsonTests/OnDeserializationErrorCallbackTest.cs b/tests/ServiceStack.Text.Tests/JsonTests/OnDeserializationErrorCallbackTest.cs new file mode 100644 index 000000000..c183eaac0 --- /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 OnDeserializationErrorTest + { + [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.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)); + } + + [Test] + public void TestReset() + { + JsConfig.Reset(); + Assert.IsNull(JsConfig.OnDeserializationError); + JsConfig.OnDeserializationError = (o, s, s1, arg3, arg4) => { }; + Assert.IsNotNull(JsConfig.OnDeserializationError); + JsConfig.Reset(); + Assert.IsNull(JsConfig.OnDeserializationError); + } + + [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.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)); + } + + [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.OnDeserializationError = (o, propertyType, s, s1, arg4) => + { + deserialized = o; + property = s; + value = s1; + type = propertyType; + 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/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; + } + } + } } 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/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..a79f79020 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -164,6 +164,11 @@ + + + + +