This is not implemented out of the box. If you inspect the source for JsonSerializerInternalWriter.SerializeList()
you will see there is no logic to skip collection entries based on some filter.
However, Json.NET does have robust exception handling. If an exception is thrown when beginning to serialize an object then caught and swallowed in an [OnError]
callback:
- If writing an array entry, the array entry is skipped (your desired behavior).
- If writing the root object, the exception is not caught (possibly a bug?)
- Otherwise
null
is written.
Thus one possibility to achieve your desired functionality would be to throw an exception from an artificial callback added to JsonContract.OnSerializingCallbacks
by your custom contract resolver, then catch and swallow the exception with a handler added to JsonContract.OnErrorCallbacks
. When combined with filtering on property values as you are already doing, this approach has the advantage of guaranteeing a secret object cannot be serialized even when it is the root object or when contained in a dictionary, a dynamic object, or a multidimensional array. This approach will not interfere with PreserveReferencesHandling.Arrays
.
One contract resolver that does this is as follows:
sealed class JsonSkipObjectException : JsonException
{
}
public class ShouldSerializeContractResolver : DefaultContractResolver
{
readonly Predicate<object> shouldSerialize;
readonly SerializationCallback serializationCallback;
readonly SerializationErrorCallback onErrorCallback;
public ShouldSerializeContractResolver(Predicate<object> shouldSerialize)
: base()
{
this.shouldSerialize = shouldSerialize;
this.serializationCallback = (o, context) =>
{
if (shouldSerialize != null && !this.shouldSerialize(o))
throw new JsonSkipObjectException();
};
this.onErrorCallback = (o, context, errorContext) =>
{
if (errorContext.Error is JsonSkipObjectException)
{
errorContext.Handled = true;
}
};
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (shouldSerialize != null)
{
if (property.Readable)
{
var oldShouldSerialize = property.ShouldSerialize;
property.ShouldSerialize = (o) =>
{
if (oldShouldSerialize != null && !oldShouldSerialize(o))
return false;
var value = property.ValueProvider.GetValue(o);
if (!this.shouldSerialize(value))
return false;
return true;
};
}
}
return property;
}
protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);
contract.OnSerializingCallbacks.Add(serializationCallback);
contract.OnErrorCallbacks.Add(onErrorCallback);
return contract;
}
}
Then one possible use would be:
public interface IConditionalSerialization
{
bool ShouldSerialize();
}
public class ConditionalSerializationObject : IConditionalSerialization
{
public bool IsSecret { get; set; }
public string SecretProperty { get { return "should not see me"; } }
// Ensure "normal" conditional property serialization is not broken
public bool ShouldSerializeSecretProperty()
{
return false;
}
#region IConditionalSerialization Members
bool IConditionalSerialization.ShouldSerialize()
{
return !IsSecret;
}
#endregion
}
public class TestClass
{
public static void Test()
{
Predicate<object> filter = (o) =>
{
var conditional = o as IConditionalSerialization;
return conditional == null || conditional.ShouldSerialize();
};
var settings = new JsonSerializerSettings
{
ContractResolver = new ShouldSerializeContractResolver(filter),
};
var ok = new ConditionalSerializationObject { IsSecret = false };
var notOk = new ConditionalSerializationObject { IsSecret = true };
Test(ok, settings);
Test(new { Public = ok, Private = notOk }, settings);
Test(new [] { ok, notOk, ok, notOk }, settings);
Test(new[,] {{ ok, notOk, ok, notOk }}, settings);
Test(new { Array = new[,] { { ok, notOk, ok, notOk } } }, settings);
try
{
Test(notOk, settings);
}
catch (Exception ex)
{
Console.WriteLine("Exception thrown and not caught serializing root object " + notOk.GetType());
Console.WriteLine(ex);
}
}
static void Test<T>(T value, JsonSerializerSettings settings)
{
Console.WriteLine("Unfiltered object: ");
Console.WriteLine(JToken.FromObject(value));
var serializer = JsonSerializer.CreateDefault(settings);
var token = JToken.FromObject(value, serializer);
Console.WriteLine("Filtered object: ");
Console.WriteLine(token);
if (!token.SelectTokens("..IsSecret").All(t => JToken.DeepEquals(t, (JValue)false)))
{
throw new InvalidOperationException("token.SelectTokens("..IsSecret").All(t => JToken.DeepEquals(t, (JValue)true))");
}
if (token.SelectTokens("..SecretProperty").Any())
{
throw new InvalidOperationException("token.SelectTokens("..SecretProperty").Any()");
}
Console.WriteLine("Secret objects and properties were successfully filtered.");
Console.WriteLine("");
}
}
Prototype fiddle.
Note that throwing and catching a large number of exceptions can have performance implications. See How expensive are exceptions in C#?. You will need to profile your web application to determine whether this is a problem. You also will need to decide whether your web service should return an exception when attempting to serialize a "secret" root object, or do something different.