Json.NET has no built-in functionality to filter keys from dictionaries while serializing. Therefore you will need to create a custom JsonConverter
that does the necessary filtering. Then, since you can't change your class, you will need to apply the converter using a custom contract resolver.
First, the custom JsonConverter
:
public class KeyFilteringDictionaryConverter<TKey, TValue> : JsonConverter<IDictionary<TKey, TValue>>
{
readonly HashSet<TKey> toSkip;
public KeyFilteringDictionaryConverter(IEnumerable<TKey> toSkip) => this.toSkip = toSkip?.ToHashSet() ?? throw new ArgumentNullException(nameof(toSkip));
public override void WriteJson(JsonWriter writer, IDictionary<TKey, TValue> value, JsonSerializer serializer) => serializer.Serialize(writer, new KeyFilteringDictionarySurrogate<TKey, TValue>(value, toSkip));
public override bool CanRead => false;
public override IDictionary<TKey, TValue> ReadJson(JsonReader reader, Type objectType, IDictionary<TKey, TValue> existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException();
}
public class KeyFilteringDictionarySurrogate<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
{
readonly IDictionary<TKey, TValue> dictionary;
readonly HashSet<TKey> toSkip;
public KeyFilteringDictionarySurrogate(IDictionary<TKey, TValue> dictionary, IEnumerable<TKey> toSkip) : this(dictionary, toSkip ?.ToHashSet()) { }
public KeyFilteringDictionarySurrogate(IDictionary<TKey, TValue> dictionary, HashSet<TKey> toSkip)
{
this.dictionary = dictionary ?? throw new ArgumentNullException(nameof(dictionary));
this.toSkip = toSkip ?? throw new ArgumentNullException(nameof(toSkip));
}
public bool ContainsKey(TKey key) => !toSkip.Contains(key) && dictionary.ContainsKey(key);
public bool TryGetValue(TKey key, out TValue value)
{
if (toSkip.Contains(key))
{
value = default(TValue);
return false;
}
return dictionary.TryGetValue(key, out value);
}
public TValue this[TKey key] => toSkip.Contains(key) ? throw new KeyNotFoundException() : dictionary[key];
public IEnumerable<TKey> Keys => this.Select(p => p.Key);
public IEnumerable<TValue> Values => this.Select(p => p.Value);
public int Count => this.Count(); // Could be made faster?
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => dictionary.Where(p => !toSkip.Contains(p.Key)).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Next, the custom ContractResolver
. The easiest place to apply the custom converter would be in CreateProperties()
after all the property information has been created. Overriding CreateObjectContract()
would also work.
public class IgnorePrivatePropertiesContractResolver : DefaultContractResolver
{
protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
{
var jsonProperties = base.CreateProperties(type, memberSerialization);
// Apply to all string-keyed dictionary properties named "Data" that do not already have a converter
foreach (var dataProperty in jsonProperties.Where(p => p.PropertyName == "Data" && p.Converter == null))
{
var keyValuePairTypes = dataProperty.PropertyType.GetDictionaryKeyValueTypes().ToList();
if (keyValuePairTypes.Count == 1 && keyValuePairTypes[0][0] == typeof(string))
{
// Filter all properties with PrivateFieldAttribute applied
var ignoreProperties = jsonProperties.Where(p => p.AttributeProvider.GetAttributes(typeof(PrivateFieldAttribute), true).Any()).Select(p => p.PropertyName).ToHashSet();
if (ignoreProperties.Count > 0)
{
dataProperty.Converter = (JsonConverter)Activator.CreateInstance(typeof(KeyFilteringDictionaryConverter<,>).MakeGenericType(keyValuePairTypes[0]), new object [] { ignoreProperties });
}
}
}
return jsonProperties;
}
}
public static class TypeExtensions
{
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
=> (type ?? throw new ArgumentNullException()).IsInterface ? new[] { type }.Concat(type.GetInterfaces()) : type.GetInterfaces();
public static IEnumerable<Type []> GetDictionaryKeyValueTypes(this Type type)
=> type.GetInterfacesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IDictionary<,>)).Select(t => t.GetGenericArguments());
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple=false)]
public class PrivateFieldAttribute : System.Attribute { }
Finally, use e.g. as follows:
var test = new Test
{
Data = new Dictionary<string, string>
{
{"SomeAdditionalKey", "Some additional value"},
},
Name = "private value",
NotPrivate = "public value",
};
var resolver = new IgnorePrivatePropertiesContractResolver();
var settings = new JsonSerializerSettings
{
ContractResolver = resolver,
};
var json = JsonConvert.SerializeObject(test, Formatting.Indented, settings);
Notes:
This solution doesn't actually remove the private key from the dictionary, it simply skips serializing it. Generally serialization doesn't actually modify the object being serialized. If you really need to actually modify the dictionary during serialization your contract resolver could apply a custom OnSerializing()
method to do that, instead of applying a converter.
Your question doesn't specify exactly how to determine the properties to which KeyFilteringDictionaryConverter
should be applied. I applied it to all Dictionary<string, TValue>
properties named "Data"
when there is also a member with PrivateFieldAttribute
in the same class. You could restrict it to just Test
, or use any other logic that suits your needs, as long as the converter is applied only to a property of type IDictionary<string, TValue>
for any type TValue
.
You may want to statically cache the contract resolver for best performance.
The converter does not attempt to remove private properties while deserializing as this was not required by your question.
Your Test
class, when serialized, includes the value of NotPrivate
twice: once as a property, and once as a member of the dictionary property:
{
"Data": {
"SomeAdditionalKey": "Some additional value",
"NotPrivate": "public value"
},
"NotPrivate": "public value"
}
You may want to exclude it from one location or the other.
You wrote, I can't get the Dictionary out of JsonProperty.
That is correct. A contract resolver defines how to map .Net types to JSON. As such, specific instances are not available during contract creation. That's why it is necessary to apply a custom converter, as specific instances are passed into the converter during serialization and deserialization.
Demo fiddle here.