You have a couple of additional issues with your current architecture other than the problem you have identified:
You are ignoring the documented recommendations for DefaultValueAttribute:
A DefaultValueAttribute will not cause a member to be automatically initialized with the attribute's value. You must set the initial value in your code.
Your current implementation causes all instances of A with default values to share a reference to a single global instance of the int[3] { 4, 6, 12 } array. Since arrays aren't really read-only, this means that modifying one instance of A will modify all other current and future instances of A with default values:
var serializerSettings = new JsonSerializerSettings
{
DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
};
var a1 = JsonConvert.DeserializeObject<A>("{}", serializerSettings);
// The following succeeds
Assert.IsTrue(a1.SomeArray.SequenceEqual(new int[] { 4, 6, 12 }));
// Sime SomeArray is a globally shared pointer, this will modify all current and future instances of A!
a1.SomeArray[0] = -999;
var a2 = JsonConvert.DeserializeObject<A>("{}", serializerSettings);
// The following now fails!
Assert.IsTrue(a2.SomeArray.SequenceEqual(new int[] { 4, 6, 12 }));
This simplest way to avoid these problems is not to use DefaultValueHandling for arrays at all, and instead use conditional property serialization:
class A
{
static readonly int[] SomeArrayDefaultValue = new int[] { 4, 6, 12 };
// Disable global settings for NullValueHandling and DefaultValueHandling
[JsonProperty(NullValueHandling = NullValueHandling.Include, DefaultValueHandling = DefaultValueHandling.Include)]
public int[] SomeArray = (int[])SomeArrayDefaultValue.Clone();
public bool ShouldSerializeSomeArray()
{
return !(SomeArray != null && SomeArray.SequenceEqual(SomeArrayDefaultValue));
}
}
Demo fiddle #1 here.
If you are determined to use DefaultValueHandling and DefaultValueAttribute for arrays, you will need a custom contract resolver:
public class ArrayDefaultValueContractResolver : DefaultContractResolver
{
class ArrayDefaultValueProvider : IValueProvider
{
readonly IValueProvider baseProvider;
readonly System.Array defaultValue;
public ArrayDefaultValueProvider(IValueProvider baseProvider, System.Array defaultValue)
{
this.baseProvider = baseProvider;
this.defaultValue = defaultValue;
}
#region IValueProvider Members
public object GetValue(object target)
{
return baseProvider.GetValue(target);
}
public void SetValue(object target, object value)
{
// Make sure the default value is cloned since arrays are not truly read only.
if (value != null && object.ReferenceEquals(value, defaultValue))
value = defaultValue.Clone();
baseProvider.SetValue(target, value);
}
#endregion
}
static void AddArrayDefaultHandling<T>(JsonProperty property)
{
var defaultValue = (T [])property.DefaultValue;
// If the default value has length > 0, clone it when setting it back into the object.
if (defaultValue.Length > 0)
{
property.ValueProvider = new ArrayDefaultValueProvider(property.ValueProvider, defaultValue);
}
// Add a ShouldSerialize method that checks for memberwise array equality.
var valueProvider = property.ValueProvider;
var oldShouldSerialize = property.ShouldSerialize;
Predicate<object> shouldSerialize = target =>
{
var array = (T[])valueProvider.GetValue(target);
return !(array == null || array.SequenceEqual(defaultValue));
};
if (oldShouldSerialize == null)
property.ShouldSerialize = shouldSerialize;
else
property.ShouldSerialize = (target) => shouldSerialize(target) && oldShouldSerialize(target);
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (property.PropertyType.IsArray && property.DefaultValue != null && property.DefaultValue.GetType() == property.PropertyType
&& property.PropertyType.GetArrayRank() == 1)
{
typeof(ArrayDefaultValueContractResolver)
.GetMethod("AddArrayDefaultHandling", BindingFlags.Static | BindingFlags.NonPublic)
.MakeGenericMethod(property.PropertyType.GetElementType())
.Invoke(null, BindingFlags.Static | BindingFlags.NonPublic, null, new [] { property }, null);
}
return property;
}
}
To use it, cache a static instance somewhere for performance, e.g.
static IContractResolver resolver = new ArrayDefaultValueContractResolver();
And use it as JsonSerializerSettings.ContractResolver when serializing:
var serializerSettings = new JsonSerializerSettings
{
DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
ContractResolver = resolver,
};
var a = new A();
JsonConvert.PopulateObject("{}", a, serializerSettings);
Console.WriteLine(JsonConvert.SerializeObject(a, serializerSettings));
Assert.IsTrue(JsonConvert.SerializeObject(a, serializerSettings) == "{}");
a.SomeArray = new int[] { 4, 6, 12 };
Console.WriteLine(JsonConvert.SerializeObject(a, serializerSettings));
Assert.IsTrue(JsonConvert.SerializeObject(a, serializerSettings) == "{}");
Demo fiddle #2 here.
Notes:
The contract resolver is only implemented for arrays of rank 1. You could extend it to multidimensional arrays if required.
The contract resolver automatically clones the default value array instance when setting it into a member, to avoid problem #2 mentioned above. If you don't want that you can remove ArrayDefaultValueProvider.
It's not really clear that support for array-valued default values is an intended functionality of Json.NET.