bboyle1234
bboyle1234

Reputation: 5009

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"

Upvotes: 11

Views: 2920

Answers (3)

dbc
dbc

Reputation: 116532

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.

Upvotes: 10

Tim Rogers
Tim Rogers

Reputation: 21713

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; }
}

Upvotes: 12

bboyle1234
bboyle1234

Reputation: 5009

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;
        }
    }

Upvotes: 2

Related Questions