Skip to content

Commit 098d1d8

Browse files
committed
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.
1 parent b91fa60 commit 098d1d8

File tree

7 files changed

+181
-19
lines changed

7 files changed

+181
-19
lines changed

src/ServiceStack.Text/Common/DeserializeType.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ public static ParseStringDelegate GetParseMethod(TypeConfig typeConfig)
4040
return value => ctorFn();
4141

4242
return typeof(TSerializer) == typeof(Json.JsonTypeSerializer)
43-
? (ParseStringDelegate)(value => DeserializeTypeRefJson.StringToType(type, value, ctorFn, map))
44-
: value => DeserializeTypeRefJsv.StringToType(type, value, ctorFn, map);
43+
? (ParseStringDelegate)(value => DeserializeTypeRefJson.StringToType(typeConfig, value, ctorFn, map))
44+
: value => DeserializeTypeRefJsv.StringToType(typeConfig, value, ctorFn, map);
4545
}
4646

4747
public static object ObjectStringToType(string strType)

src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,13 @@ internal static class DeserializeTypeRefJson
5252
private static readonly JsonTypeSerializer Serializer = (JsonTypeSerializer)JsonTypeSerializer.Instance;
5353

5454
internal static object StringToType(
55-
Type type,
55+
TypeConfig typeConfig,
5656
string strType,
5757
EmptyCtorDelegate ctorFn,
5858
Dictionary<string, TypeAccessor> typeAccessorMap)
5959
{
6060
var index = 0;
61+
var type = typeConfig.Type;
6162

6263
if (strType == null)
6364
return null;
@@ -140,6 +141,10 @@ internal static object StringToType(
140141
var parseFn = JsonReader.GetParseFn(propType);
141142

142143
var propertyValue = parseFn(propertyValueStr);
144+
if (typeConfig.OnDeserializingMember != null)
145+
{
146+
propertyValue = typeConfig.OnDeserializingMember(instance, propertyName, propertyValue);
147+
}
143148
typeAccessor.SetProperty(instance, propertyValue);
144149
}
145150

@@ -167,6 +172,10 @@ internal static object StringToType(
167172
try
168173
{
169174
var propertyValue = typeAccessor.GetProperty(propertyValueStr);
175+
if (typeConfig.OnDeserializingMember != null)
176+
{
177+
propertyValue = typeConfig.OnDeserializingMember(instance, propertyName, propertyValue);
178+
}
170179
typeAccessor.SetProperty(instance, propertyValue);
171180
}
172181
catch (Exception e)
@@ -175,6 +184,11 @@ internal static object StringToType(
175184
else Tracer.Instance.WriteWarning("WARN: failed to set property {0} with: {1}", propertyName, propertyValueStr);
176185
}
177186
}
187+
else if (typeConfig.OnDeserializingMember != null)
188+
{
189+
// the property is not known by the DTO
190+
typeConfig.OnDeserializingMember(instance, propertyName, propertyValueStr);
191+
}
178192

179193
//Serializer.EatItemSeperatorOrMapEndChar(strType, ref index);
180194
for (; index < strType.Length; index++) { var c = strType[index]; if (c >= JsonTypeSerializer.WhiteSpaceFlags.Length || !JsonTypeSerializer.WhiteSpaceFlags[c]) break; } //Whitespace inline

src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@ internal static class DeserializeTypeRefJsv
1212
private static readonly JsvTypeSerializer Serializer = (JsvTypeSerializer)JsvTypeSerializer.Instance;
1313

1414
internal static object StringToType(
15-
Type type,
15+
TypeConfig typeConfig,
1616
string strType,
1717
EmptyCtorDelegate ctorFn,
1818
Dictionary<string, TypeAccessor> typeAccessorMap)
1919
{
2020
var index = 0;
21+
var type = typeConfig.Type;
2122

2223
if (strType == null)
2324
return null;
@@ -97,6 +98,10 @@ internal static object StringToType(
9798
{
9899
var parseFn = Serializer.GetParseFn(propType);
99100
var propertyValue = parseFn(propertyValueStr);
101+
if (typeConfig.OnDeserializingMember != null)
102+
{
103+
propertyValue = typeConfig.OnDeserializingMember(instance, propertyName, propertyValue);
104+
}
100105
typeAccessor.SetProperty(instance, propertyValue);
101106
}
102107

@@ -117,6 +122,10 @@ internal static object StringToType(
117122
try
118123
{
119124
var propertyValue = typeAccessor.GetProperty(propertyValueStr);
125+
if (typeConfig.OnDeserializingMember != null)
126+
{
127+
propertyValue = typeConfig.OnDeserializingMember(instance, propertyName, propertyValue);
128+
}
120129
typeAccessor.SetProperty(instance, propertyValue);
121130
}
122131
catch (Exception e)
@@ -125,6 +134,11 @@ internal static object StringToType(
125134
else Tracer.Instance.WriteWarning("WARN: failed to set property {0} with: {1}", propertyName, propertyValueStr);
126135
}
127136
}
137+
else if (typeConfig.OnDeserializingMember != null)
138+
{
139+
// the property is not known by the DTO
140+
typeConfig.OnDeserializingMember(instance, propertyName, propertyValueStr);
141+
}
128142

129143
//Serializer.EatItemSeperatorOrMapEndChar(strType, ref index);
130144
if (index != strType.Length) index++;

src/ServiceStack.Text/Common/WriteType.cs

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ static Func<T, bool> GetShouldSerializeMethod(MemberInfo member)
108108
null, Type.EmptyTypes, null);
109109
return (method == null || method.ReturnType != typeof(bool)) ? null : (Func<T,bool>)Delegate.CreateDelegate(typeof(Func<T,bool>), method);
110110
}
111+
112+
static Func<T,string, bool?> ShouldSerializeByName(Type type)
113+
{
114+
var method = type.GetMethod("ShouldSerializeByName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[] { typeof(string) }, null);
115+
return (method == null || method.ReturnType != typeof(bool?))
116+
? null
117+
: (Func<T, string, bool?>)Delegate.CreateDelegate(typeof(Func<T,string, bool?>), method);
118+
}
119+
120+
111121
private static bool Init()
112122
{
113123
if (!typeof(T).IsClass() && !typeof(T).IsInterface() && !JsConfig.TreatAsRefType(typeof(T))) return false;
@@ -124,6 +134,8 @@ private static bool Init()
124134
return typeof(T).IsDto();
125135
}
126136

137+
var shouldSerializeByName = ShouldSerializeByName(typeof(T));
138+
127139
// NOTE: very limited support for DataContractSerialization (DCS)
128140
// NOT supporting Serializable
129141
// 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()
174186
propertyInfo.GetValueGetter<T>(),
175187
Serializer.GetWriteFn(propertyType),
176188
propertyType.GetDefaultValue(),
177-
shouldSerialize
189+
shouldSerialize,
190+
shouldSerializeByName
178191
);
179192
}
180193

@@ -221,7 +234,8 @@ private static bool Init()
221234
fieldInfo.GetValueGetter<T>(),
222235
Serializer.GetWriteFn(propertyType),
223236
defaultValue,
224-
shouldSerialize
237+
shouldSerialize,
238+
shouldSerializeByName
225239
);
226240
}
227241
PropertyWriters = PropertyWriters.OrderBy(x => x.propertyOrder).ToArray();
@@ -253,9 +267,10 @@ internal string PropertyName
253267
internal readonly WriteObjectDelegate WriteFn;
254268
internal readonly object DefaultValue;
255269
internal readonly Func<T, bool> shouldSerialize;
270+
internal readonly Func<T, string, bool?> shouldSerializeByName;
256271

257272
public TypePropertyWriter(string propertyName, string propertyReflectedName, string propertyNameCLSFriendly, string propertyNameLowercaseUnderscore, int propertyOrder, bool propertySuppressDefaultConfig,bool propertySuppressDefaultAttribute,
258-
Func<T, object> getterFn, WriteObjectDelegate writeFn, object defaultValue, Func<T, bool> shouldSerialize)
273+
Func<T, object> getterFn, WriteObjectDelegate writeFn, object defaultValue, Func<T, bool> shouldSerialize, Func<T,string, bool?> shouldSerializeByName)
259274
{
260275
this.propertyName = propertyName;
261276
this.propertyOrder = propertyOrder;
@@ -269,6 +284,7 @@ public TypePropertyWriter(string propertyName, string propertyReflectedName, str
269284
this.WriteFn = writeFn;
270285
this.DefaultValue = defaultValue;
271286
this.shouldSerialize = shouldSerialize;
287+
this.shouldSerializeByName = shouldSerializeByName;
272288
}
273289
}
274290

@@ -337,21 +353,38 @@ public static void WriteProperties(TextWriter writer, object value)
337353
var propertyWriter = PropertyWriters[index];
338354

339355
if (propertyWriter.shouldSerialize != null && !propertyWriter.shouldSerialize((T)value)) continue;
340-
356+
bool dontSkipDefault=false;
357+
if (propertyWriter.shouldSerializeByName != null)
358+
{
359+
var shouldSerialize = propertyWriter.shouldSerializeByName((T) value, propertyWriter.PropertyName);
360+
if (shouldSerialize.HasValue)
361+
{
362+
if (shouldSerialize.Value)
363+
{
364+
dontSkipDefault = true;
365+
}
366+
else
367+
{
368+
continue;
369+
}
370+
}
371+
}
341372
var propertyValue = value != null
342373
? propertyWriter.GetterFn((T)value)
343374
: null;
344-
345-
if (propertyWriter.propertySuppressDefaultAttribute && Equals(propertyWriter.DefaultValue, propertyValue))
346-
{
347-
continue;
348-
}
349-
if ((propertyValue == null
350-
|| (propertyWriter.propertySuppressDefaultConfig && Equals(propertyWriter.DefaultValue, propertyValue)))
351-
&& !Serializer.IncludeNullValues
352-
)
375+
if (!dontSkipDefault)
353376
{
354-
continue;
377+
if (propertyWriter.propertySuppressDefaultAttribute && Equals(propertyWriter.DefaultValue, propertyValue))
378+
{
379+
continue;
380+
}
381+
if ((propertyValue == null
382+
|| (propertyWriter.propertySuppressDefaultConfig && Equals(propertyWriter.DefaultValue, propertyValue)))
383+
&& !Serializer.IncludeNullValues
384+
)
385+
{
386+
continue;
387+
}
355388
}
356389

357390
if (exclude.Any() && exclude.Contains(propertyWriter.propertyCombinedNameUpper)) continue;

src/ServiceStack.Text/ReflectionExtensions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,17 @@ public static PropertyInfo[] GetSerializableProperties(this Type type)
536536
return publicReadableProperties.Where(prop => !prop.CustomAttributes(false).Any(attr => attr.GetType().Name == IgnoreDataMember)).ToArray();
537537
}
538538

539+
public static Func<object, string, object, object> GetOnDeserializingMember<T>()
540+
{
541+
var method = typeof(T).GetMethod("OnDeserializingMember", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new[] { typeof(string), typeof(object) }, null);
542+
if (method == null || method.ReturnType != typeof (object))
543+
{
544+
return null;
545+
}
546+
var ojb = (Func<T, string, object, object>)Delegate.CreateDelegate(typeof(Func<T, string, object, object>), method);
547+
return (instance, memberName, value) => ojb((T)instance, memberName, value);
548+
}
549+
539550
public static FieldInfo[] GetSerializableFields(this Type type)
540551
{
541552
if (type.IsDto()) {

src/ServiceStack.Text/TypeConfig.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal class TypeConfig
1010
internal bool EnableAnonymousFieldSetterses;
1111
internal PropertyInfo[] Properties;
1212
internal FieldInfo[] Fields;
13+
internal Func<object, string, object, object> OnDeserializingMember;
1314

1415
internal TypeConfig(Type type)
1516
{
@@ -42,6 +43,12 @@ public static bool EnableAnonymousFieldSetters
4243
set { config.EnableAnonymousFieldSetterses = value; }
4344
}
4445

46+
public static Func<object, string, object, object> OnDeserializingMember
47+
{
48+
get { return config.OnDeserializingMember; }
49+
set { config.OnDeserializingMember = value; }
50+
}
51+
4552
static TypeConfig()
4653
{
4754
config = new TypeConfig(typeof(T));
@@ -54,6 +61,7 @@ static TypeConfig()
5461
Properties = properties.Where(x => x.GetIndexParameters().Length == 0).ToArray();
5562

5663
Fields = config.Type.GetSerializableFields().ToArray();
64+
OnDeserializingMember = ReflectionExtensions.GetOnDeserializingMember<T>();
5765
}
5866

5967
internal static TypeConfig GetState()

tests/ServiceStack.Text.Tests/JsonTests/ConditionalSerializationTests.cs

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
using NUnit.Framework;
1+
using System.Reflection;
2+
using System.Runtime.Serialization;
3+
using NUnit.Framework;
24
using System;
5+
using ServiceStack.Text.WP;
36

47
namespace ServiceStack.Text.Tests.JsonTests
58
{
@@ -14,6 +17,7 @@ public void TestSerializeRespected()
1417
string json = JsonSerializer.SerializeToString(obj);
1518
Assert.That(json, Is.StringMatching("{\"X\":\"abc\",\"Z\":\"def\"}"));
1619
}
20+
1721
[Test]
1822
public void TestSerializeRespectedWithInheritance()
1923
{
@@ -22,6 +26,52 @@ public void TestSerializeRespectedWithInheritance()
2226
string json = JsonSerializer.SerializeToString(obj);
2327
Assert.That(json, Is.StringMatching("{\"A\":123,\"C\":456,\"X\":\"abc\",\"Z\":\"def\"}"));
2428
}
29+
30+
[Test]
31+
public void TestSerializeHasAttributeNull()
32+
{
33+
var obj = new ByNameFoo { A = 123, B = 456 };
34+
35+
string json = JsonSerializer.SerializeToString(obj);
36+
Assert.That(json, Is.StringMatching("{\"A\":123,\"B\":456}"));
37+
}
38+
39+
[Test]
40+
public void TestSerializeHasAttributeSet()
41+
{
42+
var obj = new ByNameFoo {A = 123, B = 456, hasAttribute = new HashSet<string> {"A"}};
43+
44+
string json = JsonSerializer.SerializeToString(obj);
45+
Assert.That(json, Is.StringMatching("{\"A\":123"));
46+
47+
var cpy = JsonSerializer.DeserializeFromString<ByNameFoo>(json);
48+
Assert.That(cpy.A, Is.EqualTo(obj.A));
49+
Assert.That(cpy.hasAttribute, Is.EqualTo(obj.hasAttribute));
50+
}
51+
52+
[Test]
53+
public void TestSerializeHasAttributeSetNullValue()
54+
{
55+
var obj = new ByNameFoo {A = 123, B = null, hasAttribute = new HashSet<string> {"B"}};
56+
57+
string json = JsonSerializer.SerializeToString(obj);
58+
Assert.That(json, Is.StringMatching("{\"B\":null"));
59+
60+
var cpy = JsonSerializer.DeserializeFromString<ByNameFoo>(json);
61+
Assert.That(cpy.A, Is.EqualTo(0));
62+
Assert.That(cpy.B, Is.EqualTo(obj.B));
63+
Assert.That(cpy.hasAttribute, Is.EqualTo(obj.hasAttribute));
64+
}
65+
66+
[Test]
67+
public void OnDeserializingMemberUnknown()
68+
{
69+
var cpy = JsonSerializer.DeserializeFromString<ByNameFoo>("{\"B\":1,\"C\":1");
70+
Assert.That(cpy.A, Is.EqualTo(0));
71+
Assert.That(cpy.B, Is.EqualTo(1));
72+
Assert.That(cpy.hasAttribute, Is.EqualTo(new HashSet<string>{"B","C"}));
73+
}
74+
2575
public class Foo
2676
{
2777
public string X { get; set; } // not conditional
@@ -65,5 +115,37 @@ public bool ShouldSerializeC()
65115
return true;
66116
}
67117
}
118+
119+
[DataContract]
120+
public class ByNameFoo
121+
{
122+
[IgnoreDataMember]
123+
public HashSet<string> hasAttribute;
124+
125+
[DataMember(EmitDefaultValue = false,IsRequired = false)]
126+
public int A { get; set; }
127+
128+
[DataMember(EmitDefaultValue = false, IsRequired = false)]
129+
public int? B { get; set; }
130+
131+
private bool? ShouldSerializeByName(string fieldName)
132+
{
133+
if (hasAttribute == null)
134+
{
135+
return null;
136+
}
137+
return hasAttribute.Contains(fieldName);
138+
}
139+
140+
private object OnDeserializingMember(string fieldName, object value)
141+
{
142+
if (hasAttribute == null)
143+
{
144+
hasAttribute=new HashSet<string>();
145+
}
146+
hasAttribute.Add(fieldName);
147+
return value;
148+
}
149+
}
68150
}
69151
}

0 commit comments

Comments
 (0)