Serializing a null value in JSON.NET

When serializing arbitrary data through JSON.NET, any property that is null is written to JSON as

"propertyName": null

This is correct, of course.

However, I have a requirement to automatically set all zeros to the default value, equal to the default. null string should become String.Empty , null int? should become 0 , null bool? must be false , etc.

NullValueHandling doesn't help, since I don't want Ignore nulls, but I also don't want Include them (Hmm, new function?).

So, I turned to the implementation of custom JsonConverter .
While the implementation itself was easy, unfortunately, it still didn't work - CanConvert() never called for a property with a null value, so WriteJson() is not called either. Apparently, zeros are automatically serialized directly to null without a custom pipeline.

For example, here is an example of a custom converter for null strings:

 public class StringConverter : JsonConverter { public override bool CanConvert(Type objectType) { return typeof(string).IsAssignableFrom(objectType); } ... public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { string strValue = value as string; if (strValue == null) { writer.WriteValue(String.Empty); } else { writer.WriteValue(strValue); } } } 

When doing this in the debugger, I noticed that none of these methods were called for properties with a null value.

Getting into the JSON.NET source code, I found that (apparently, I did not deal with great depth) there is a special register for checking zeros and an explicit call to .WriteNull() .

For what it's worth, I tried to implement a custom JsonTextWriter and override the default implementation .WriteNull() ...

 public class NullJsonWriter : JsonTextWriter { ... public override void WriteNull() { this.WriteValue(String.Empty); } } 

However, this will not work, as the WriteNull() method does not know anything about the underlying data type. I am sure that I can output "" for any null value, but this does not work well, for example. int, bool, etc.

So my question is - do I need to convert the entire data structure manually, is there any solution or workaround for this?

+45
c # serialization
Jan 12 '12 at 11:00
source share
1 answer

Well, I think I came up with a solution (my first decision was not entirely correct, but again I was on the train). You need to create a special contract converter and a custom ValueProvider for Nullable types. Consider this:

 public class NullableValueProvider : IValueProvider { private readonly object _defaultValue; private readonly IValueProvider _underlyingValueProvider; public NullableValueProvider(MemberInfo memberInfo, Type underlyingType) { _underlyingValueProvider = new DynamicValueProvider(memberInfo); _defaultValue = Activator.CreateInstance(underlyingType); } public void SetValue(object target, object value) { _underlyingValueProvider.SetValue(target, value); } public object GetValue(object target) { return _underlyingValueProvider.GetValue(target) ?? _defaultValue; } } public class SpecialContractResolver : DefaultContractResolver { protected override IValueProvider CreateMemberValueProvider(MemberInfo member) { if(member.MemberType == MemberTypes.Property) { var pi = (PropertyInfo) member; if (pi.PropertyType.IsGenericType && pi.PropertyType.GetGenericTypeDefinition() == typeof (Nullable<>)) { return new NullableValueProvider(member, pi.PropertyType.GetGenericArguments().First()); } } else if(member.MemberType == MemberTypes.Field) { var fi = (FieldInfo) member; if(fi.FieldType.IsGenericType && fi.FieldType.GetGenericTypeDefinition() == typeof(Nullable<>)) return new NullableValueProvider(member, fi.FieldType.GetGenericArguments().First()); } return base.CreateMemberValueProvider(member); } } 

Then I tested it using:

 class Foo { public int? Int { get; set; } public bool? Boolean { get; set; } public int? IntField; } 

And the following case:

 [TestFixture] public class Tests { [Test] public void Test() { var foo = new Foo(); var settings = new JsonSerializerSettings { ContractResolver = new SpecialContractResolver() }; Assert.AreEqual( JsonConvert.SerializeObject(foo, Formatting.None, settings), "{\"IntField\":0,\"Int\":0,\"Boolean\":false}"); } } 

Hope this helps a bit ...

Edit - Best type identification a Nullable<>

Edit - Added support for fields and properties, as well as support for copies on top of the usual DynamicValueProvider to do most of the work with the updated test

+25
Jan 12 '12 at 12:40
source share



All Articles