Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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"

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

bboyle1234


2 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 99
dbc Avatar answered Oct 02 '22 14: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 34
Tim Rogers Avatar answered Oct 02 '22 16:10

Tim Rogers