Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Json.NET backward compatibility: write new property name, but read either new or old property [duplicate]

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"

like image 747
bboyle1234 Avatar asked Oct 15 '15 18:10

bboyle1234


3 Answers

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:

  • This implementation doesn't require that the class have explicit data contract attribute annotation. You could add that restriction, if you prefer.

  • You should cache and reuse instances of contract resolvers for best performance.

Demo fiddle here.

like image 131
dbc Avatar answered Oct 03 '22 00:10

dbc


A very simple solution using Json.NET is to just provide a legacy property with a setter only.

class TestObject {
    public int A { get; set; }
    public int alpha { set => A = value; }
    public int omega { set => A = value; }
}

You'd probably rather not have these public, in which case you can just mark private and add the JsonProperty attribute.

class TestObject {
    public int A { get; set; }
    [JsonProperty] private int alpha { set => A = value; }
    [JsonProperty] private int omega { set => A = value; }
}
like image 37
Tim Rogers Avatar answered Oct 03 '22 02:10

Tim Rogers


I took your code and modified it toward my own styling, like this:

    [System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
    public class LegacyDataMemberNamesAttribute : Attribute {

        public readonly string[] LegacyNames;

        public LegacyDataMemberNamesAttribute(params string[] legacyNames) {
            LegacyNames = legacyNames;
        }
    }

    public class LegacyPropertyResolver : DefaultContractResolver {

        // As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
        // http://www.newtonsoft.com/json/help/html/ContractResolver.htm
        // http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
        // "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."

        public static readonly LegacyPropertyResolver Instance = new LegacyPropertyResolver();

        protected LegacyPropertyResolver() : base() { }

        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization) {
            var properties = base.CreateProperties(type, memberSerialization);
            foreach (var property in properties.ToArray()) {
                if (!property.Writable) continue;
                foreach (var legacyName in GetLegacyNames(property)) {
                    properties.Add(CloneWithLegacyName(property, legacyName));
                }
            }
            return properties;
        }

        static IEnumerable<string> GetLegacyNames(JsonProperty property) {
            return property.AttributeProvider.GetAttributes(typeof(LegacyDataMemberNamesAttribute), true)
                    .Cast<LegacyDataMemberNamesAttribute>()
                    .SelectMany(a => a.LegacyNames)
                    .Distinct();
        }

        static readonly object[] _emptyObjectArray = new object[0];
        static readonly MethodInfo _propertyClone = typeof(JsonProperty).GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
        static JsonProperty CloneWithLegacyName(JsonProperty property, string legacyName) {
            var legacyProperty = (JsonProperty)_propertyClone.Invoke(property, _emptyObjectArray);
            legacyProperty.Readable = false;
            legacyProperty.PropertyName = legacyName;
            return legacyProperty;
        }
    }
like image 42
bboyle1234 Avatar answered Oct 03 '22 01:10

bboyle1234