Search code examples
c#jsonjson.netjson-deserialization

json deserialize from legacy property names


How can I setup Newtonsoft.Json to deserialize an object using legacy member names but serialize it using the current member name?

Edit: A requirement is that the obsolete member be removed from the class being serialized/deserialized.

Here's an example object that needs to be serialized and deserialized. I've given a property an attribute containing a list of names that it may have been serialized under in the past.

[DataContract]
class TestObject {
    [LegacyDataMemberNames("alpha", "omega")]
    [DataMember(Name = "a")]
    public int A { get; set; }
}

I'd like to json serialize always using name "a" but be able to deserialize to the one property from any legacy name including "alpha" and "omega" as well as the current name, "a"


Solution

  • This can be done with a custom IContractResolver created by extending DefaultContractResolver:

    [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
    public class LegacyDataMemberNamesAttribute : Attribute
    {
        public LegacyDataMemberNamesAttribute() : this(new string[0]) { }
    
        public LegacyDataMemberNamesAttribute(params string[] names) { this.Names = names; }
    
        public string [] Names { get; set; }
    }
    
    public class LegacyPropertyResolver : DefaultContractResolver
    {
        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
        {
            var properties = base.CreateProperties(type, memberSerialization);
    
            for (int i = 0, n = properties.Count; i < n; i++)
            {
                var property = properties[i];
                if (!property.Writable)
                    continue;
                var attrs = property.AttributeProvider.GetAttributes(typeof(LegacyDataMemberNamesAttribute), true);
                if (attrs == null || attrs.Count == 0)
                    continue;
                // Little kludgy here: use MemberwiseClone to clone the JsonProperty.
                var clone = property.GetType().GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
                foreach (var name in attrs.Cast<LegacyDataMemberNamesAttribute>().SelectMany(a => a.Names))
                {
                    if (properties.Any(p => p.PropertyName == name))
                    {
                        Debug.WriteLine("Duplicate LegacyDataMemberNamesAttribute: " + name);
                        continue;
                    }
                    var newProperty = (JsonProperty)clone.Invoke(property, new object[0]);
                    newProperty.Readable = false;
                    newProperty.PropertyName = name;
                    properties.Add(newProperty);
                }
            }
    
            return properties;
        }
    }
    

    Then add attributes to your type as shown in the question:

    [DataContract]
    class TestObject
    {
        [LegacyDataMemberNames("alpha", "omega")]
        [DataMember(Name = "a")]
        public int A { get; set; }
    }
    

    Construct and configure an instance of LegacyPropertyResolver, e.g. as follows:

    static IContractResolver legacyResolver = new LegacyPropertyResolver 
    { 
        // Configure as required, e.g. 
        // NamingStrategy = new CamelCaseNamingStrategy() 
    };
    

    And then use it in settings:

    var settings = new JsonSerializerSettings { ContractResolver = legacyResolver };
    var deserialized = JsonConvert.DeserializeObject<TestObject>(jsonString, settings);
    

    Notes:

    Demo fiddle here.