0

I have the following class (I don't want to change the class to solve the issue..):

public class Test
{
    public Dictionary<string, string> Data;    

    [PrivateField]
    [JsonIgnore]
    public string Name { get { return Data["Name"]; } set { Data.Add("Name", value); } }

    public string NotPrivate { get { return Data["NotPrivate"]; } set { Data.Add("NotPrivate", value); } }
}

I want to remove specific key from Data property during serialize, in my case the key in the dictionary is 'Name' because it is marked as private.

    var test = new Test();
    var settings = new JsonSerializerSettings();
    settings.ContractResolver = new IgnorePrivatePropertiesContractResolver();

    var places = JsonConvert.SerializeObject(test, settings);


public class IgnorePrivatePropertiesContractResolver : DefaultContractResolver

in IgnorePrivatePropertiesContractResolver I have tried :

override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)

but I can't get the Dictionary out of JsonProperty.

I also tried :

JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)

but I can't get the Dictionary out of MemberInfo .

K.Moment
  • 1
  • 2
  • JsonIgnore attribute should be enough without the contract resolver. See [link](https://www.newtonsoft.com/json/help/html/PropertyJsonIgnore.htm) – Luminous_Dev Oct 27 '20 at 11:18
  • @Luminous_Dev I want to serialize public Dictionary Data; BUT to remove private keys from the dictonary – K.Moment Oct 27 '20 at 11:32

2 Answers2

0

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • thank you very much for the detailed answer, I have used ValueProvider and I will be glad to know what you think about it, I will add as answer with the code – K.Moment Nov 01 '20 at 10:12
0
public class IgnorePrivatePropertiesContractResolver : DefaultContractResolver
{

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization);

        if (!(type.Name.Equals(typeof(Test).Name)))
        {
            return properties;
        }

        var dataProperty = properties.FirstOrDefault(prop => prop.PropertyName.Equals("Data"));

        if (dataProperty == null)
            return properties;

        var privatePropertiesNames = properties.Where(prop => prop.AttributeProvider.GetAttributes(false).OfType<PrivateFieldAttribute>().Any())
            .Select(privateProperty => privateProperty.PropertyName);

        
        dataProperty.ValueProvider = new JsonPropertyExtraDataValueProvider(privatePropertiesNames);

        return properties;
    }

}

public class JsonPropertyExtraDataValueProvider : IValueProvider
{

    private IEnumerable<string> _privateKeys;

    public JsonPropertyExtraDataValueProvider(IEnumerable<string> privateKeys)
    {

        _privateKeys = privateKeys;
    }

    public void SetValue(object target, object value)
    {
        throw new NotImplementedException();
    }

    public object GetValue(object target)
    {
        Dictionary<string, string> value = ((Test)target).ExtraData.ToDictionary(pr => pr.Key, pr => pr.Value);
        foreach (var privateKey in _privateKeys)
            value.Remove(privateKey);
        return value;
    }
}
K.Moment
  • 1
  • 2