1

I have a bunch of classes that I do not have control over that were created before nullable types. The convention used in these classes is like the following:

Id
IdSpecified = false

In other words, for a given non-nullable property named "Abc", there is a separate boolean property called "AbcSpecified" which indicates whether the first property has a value.

Is there a way to treat the non-nullable properties as nullable and exclude all properties ending in Specified during serialization and deserialization? I am hoping I can do this generically, as there are more than 100 classes like this.

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
Marco
  • 2,453
  • 3
  • 25
  • 35
  • 2
    What about just creating your own versions of the classes and then have some conversion functions for changing them as you need? – musefan Nov 16 '16 at 11:31
  • There are 100+ classes – Marco Nov 16 '16 at 11:36
  • I guess then if I where you I would try and create my own custom Contract Resolver and implement the rules you want. [Check this for ideas](http://stackoverflow.com/questions/13588022/exclude-property-from-serialization-via-custom-attribute-json-net) – musefan Nov 16 '16 at 11:41
  • Either making property private or adding [NonSerialized] above property will prevent object from being serialized. – jdweng Nov 16 '16 at 11:41
  • Is there a way to check another properties value on the object during resolve? – Marco Nov 16 '16 at 11:47

1 Answers1

0

I was able to come up with a generic solution for this using a custom IContractResolver in conjunction with an IValueProvider. The contract resolver is responsible for identifying the pairs of properties "Xyz" and "XyzSpecified" on each class (I'll call these the "target" and "indicator" properties, respectively), and for each pair making sure the indicator property is excluded, while also attaching a value provider instance to the target property. The value provider, in turn, handles the decision of what gets read or written for each object instance. On serialization, it only writes out the target property if the indicator property is set to true. Conversely, on deserialization, it sets the indicator property based on the presence or absence of a value in the JSON, but it only sets the target property on the object if a value is present.

Here is the code for the custom resolver and value provider:

public class CustomResolver : DefaultContractResolver
{
    const string IndicatorKeyword = "Specified";

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        IList<JsonProperty> props = base.CreateProperties(type, memberSerialization);
        Dictionary<string, JsonProperty> dict = props.ToDictionary(p => p.UnderlyingName);
        foreach (JsonProperty prop in props)
        {
            string name = prop.UnderlyingName;
            if (name.Length > IndicatorKeyword.Length && name.EndsWith(IndicatorKeyword))
            {
                // We have an indicator property; ignore it for serialization purposes
                prop.Ignored = true;

                // Find the corresponding target property, e.g. "XyzSpecified" => "Xyz"
                string targetName = name.Substring(0, name.Length - IndicatorKeyword.Length);
                JsonProperty coProp = null;
                if (dict.TryGetValue(targetName, out coProp))
                {
                    // Create a value provider for the property pointing to the
                    // "real" target and indicator properties from the containing type
                    PropertyInfo realTarget = type.GetProperty(targetName);
                    PropertyInfo realIndicator = type.GetProperty(name);
                    coProp.ValueProvider = new CustomValueProvider(realTarget, realIndicator);
                }
            }
        }
        return props;
    }

    class CustomValueProvider : IValueProvider
    {
        PropertyInfo targetProperty;
        PropertyInfo indicatorProperty;

        public CustomValueProvider(PropertyInfo targetProperty, PropertyInfo indicatorProperty)
        {
            this.targetProperty = targetProperty;
            this.indicatorProperty = indicatorProperty;
        }

        // GetValue is called by Json.Net during serialization.
        // The target parameter has the object from which to read the value;
        // the return value is what gets written to the JSON
        public object GetValue(object target)
        {
            bool isSpecified = (bool)indicatorProperty.GetValue(target);
            return isSpecified ? targetProperty.GetValue(target) : null;
        }

        // SetValue gets called by Json.Net during deserialization.
        // The value parameter has the value read from the JSON;
        // target is the object on which to set the value.
        public void SetValue(object target, object value)
        {
            bool isSpecified = value != null;
            indicatorProperty.SetValue(target, isSpecified);
            if (isSpecified) targetProperty.SetValue(target, value);
        }
    }
}

To use the resolver, create a new JsonSerializerSettings instance, then set the ContractResolver property to a new instance of the resolver. Pass the settings to the JsonConvert.SerializeObject() or DeserializeObject() methods and everything should just work.

Here is a full round-trip demo: https://dotnetfiddle.net/i39c8d

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300