Strategies for migrating a serialized Json.NET document between versions / formats

I am using Json.Net to serialize some application data. Of course, the specifications of the applications have changed a bit, and we need to reorganize some data of the business object. What are some viable strategies for transferring previously serialized data to our new data format?

For example, suppose we have a business object like:

public class Owner
{
    public string Name {get;set;} 
}
public class LeaseInstrument
{
    public ObservableCollection<Owner> OriginalLessees {get;set;}
}

We serialize the LeaseInstrument instance to a file with Json.Net. Now we are changing our business objects as follows:

public class Owner
{
   public string Name {get;set;}
}
public class LeaseOwner
{
  public Owner Owner { get;set;}
  public string DocumentName {get;set;}
}
public class LeaseInstrument
{
    public ObservableCollection<LeaseOwner> OriginalLessees {get;set;}
}

I considered creating a custom JsonConverter for LeaseInstrument, but the ReadJson method never gets caught ... instead, an exception is thrown before the deserializer reaches this point:

Additional information: Type specified in JSON
'System.Collections.ObjectModel.ObservableCollection`1[[BreakoutLib.BO.Owner,
BreakoutLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]],
System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'
is not compatible with 'System.Collections.ObjectModel.ObservableCollection`1[[BreakoutLib.BO.LeaseOwner, BreakoutLib, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'. Path 'Is.$values[8].OriginalLessors.$type', line 3142, position 120.

, , Json.Net, JsonConverter , , !!

, JsonSerializerSettings, :

var settings = new JsonSerializerSettings
    {
      PreserveReferencesHandling = PreserveReferencesHandling.Objects,
      ContractResolver = new WritablePropertiesOnlyResolver(),
      TypeNameHandling = TypeNameHandling.All,
      ObjectCreationHandling = ObjectCreationHandling.Reuse
    };
+3
2

:

  • TypeNameHandling.All. , . . TypeNameHandling.Objects, .

    JSON, IgnoreArrayTypeConverter Json.NET $, :

    public class IgnoreCollectionTypeConverter : JsonConverter
    {
        public IgnoreCollectionTypeConverter() { }
    
        public IgnoreCollectionTypeConverter(Type ItemConverterType) 
        { 
            this.ItemConverterType = ItemConverterType; 
        }
    
        public Type ItemConverterType { get; set; }
    
        public override bool CanConvert(Type objectType)
        {
            // TODO: test with read-only collections.
            return objectType.GetCollectItemTypes().Count() == 1 && !objectType.IsDictionary() && !objectType.IsArray;
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (!CanConvert(objectType))
                throw new JsonSerializationException(string.Format("Invalid type \"{0}\"", objectType));
            if (reader.TokenType == JsonToken.Null)
                return null;
            var token = JToken.Load(reader);
            var itemConverter = (ItemConverterType == null ? null : (JsonConverter)Activator.CreateInstance(ItemConverterType, true));
            if (itemConverter != null)
                serializer.Converters.Add(itemConverter);
    
            try
            {
                return ToCollection(token, objectType, existingValue, serializer);
            }
            finally
            {
                if (itemConverter != null)
                    serializer.Converters.RemoveLast(itemConverter);
            }
        }
    
        private static object ToCollection(JToken token, Type collectionType, object existingValue, JsonSerializer serializer)
        {
            if (token == null || token.Type == JTokenType.Null)
                return null;
            else if (token.Type == JTokenType.Array)
            {
                // Here we assume that existingValue already is of the correct type, if non-null.
                existingValue = serializer.DefaultCreate<object>(collectionType, existingValue);
                token.PopulateObject(existingValue, serializer);
                return existingValue;
            }
            else if (token.Type == JTokenType.Object)
            {
                var values = token["$values"];
                if (values == null)
                    return null;
                return ToCollection(values, collectionType, existingValue, serializer);
            }
            else
            {
                throw new JsonSerializationException("Unknown token type: " + token.ToString());
            }
        }
    
        public override bool CanWrite { get { return false; } }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    
  • Owner LeaseOwner.

    JsonConverter , JSON JObject, , . JSON , Linq to JSON. JSON , LeaseOwner .

    PreserveReferencesHandling = PreserveReferencesHandling.Objects, "$ref" :

    public class OwnerToLeaseOwnerConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return typeof(LeaseOwner).IsAssignableFrom(objectType);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.Null)
                return null;
            var item = JObject.Load(reader);
            if (item["$ref"] != null)
            {
                var previous = serializer.ReferenceResolver.ResolveReference(serializer, (string)item["$ref"]);
                if (previous is LeaseOwner)
                    return previous;
                else if (previous is Owner)
                {
                    var leaseOwner = serializer.DefaultCreate<LeaseOwner>(objectType, existingValue);
                    leaseOwner.Owner = (Owner)previous;
                    return leaseOwner;
                }
                else
                {
                    throw new JsonSerializationException("Invalid type of previous object: " + previous);
                }
            }
            else
            {
                var leaseOwner = serializer.DefaultCreate<LeaseOwner>(objectType, existingValue);
                if (item["Name"] != null)
                {
                    // Convert from Owner to LeaseOwner.  If $id is present, this stores the reference mapping in the reference table for us.
                    leaseOwner.Owner = item.ToObject<Owner>(serializer);
                }
                else
                {
                    // PopulateObject.  If $id is present, this stores the reference mapping in the reference table for us.
                    item.PopulateObject(leaseOwner, serializer);
                }
                return leaseOwner;
            }
        }
    
        public override bool CanWrite { get { return false; } }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    

:

public static class JsonExtensions
{
    public static T DefaultCreate<T>(this JsonSerializer serializer, Type objectType, object existingValue)
    {
        if (serializer == null)
            throw new ArgumentNullException();
        if (existingValue is T)
            return (T)existingValue;
        return (T)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator();
    }

    public static void PopulateObject(this JToken obj, object target, JsonSerializer serializer)
    {
        if (target == null)
            throw new NullReferenceException();
        if (obj == null)
            return;
        using (var reader = obj.CreateReader())
            serializer.Populate(reader, target);
    }
}

public static class TypeExtensions
{
    /// <summary>
    /// Return all interfaces implemented by the incoming type as well as the type itself if it is an interface.
    /// </summary>
    /// <param name="type"></param>
    /// <returns></returns>
    public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
    {
        if (type == null)
            throw new ArgumentNullException();
        if (type.IsInterface)
            return new[] { type }.Concat(type.GetInterfaces());
        else
            return type.GetInterfaces();
    }

    public static IEnumerable<Type> GetCollectItemTypes(this Type type)
    {
        foreach (Type intType in type.GetInterfacesAndSelf())
        {
            if (intType.IsGenericType
                && intType.GetGenericTypeDefinition() == typeof(ICollection<>))
            {
                yield return intType.GetGenericArguments()[0];
            }
        }
    }

    public static bool IsDictionary(this Type type)
    {
        if (typeof(IDictionary).IsAssignableFrom(type))
            return true;

        foreach (Type intType in type.GetInterfacesAndSelf())
        {
            if (intType.IsGenericType
                && intType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
            {
                return true;
            }
        }
        return false;
    }
}

public static class ListExtensions
{
    public static bool RemoveLast<T>(this IList<T> list, T item)
    {
        if (list == null)
            throw new ArgumentNullException();
        var comparer = EqualityComparer<T>.Default;
        for (int i = list.Count - 1; i >= 0; i--)
        {
            if (comparer.Equals(list[i], item))
            {
                list.RemoveAt(i);
                return true;
            }
        }
        return false;
    }
}

, JsonConverterAttribute, :

public class LeaseInstrument
{
    [JsonConverter(typeof(IgnoreCollectionTypeConverter), typeof(OwnerToLeaseOwnerConverter))]
    public ObservableCollection<LeaseOwner> OriginalLessees { get; set; }
}

Json.NET , :

public class WritablePropertiesOnlyResolver : DefaultContractResolver
{
    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var result = base.CreateProperty(member, memberSerialization);
        if (typeof(LeaseInstrument).IsAssignableFrom(result.DeclaringType) && typeof(ICollection<LeaseOwner>).IsAssignableFrom(result.PropertyType))
        {
            var converter = new IgnoreCollectionTypeConverter { ItemConverterType = typeof(OwnerToLeaseOwnerConverter) };
            result.Converter = result.Converter ?? converter;
            result.MemberConverter = result.MemberConverter ?? converter;
        }
        return result;
    }
}

, .

+2

, Migrations.Json.Net.

https://github.com/Weingartner/Migrations.Json.Net

. ,

public class Person {
   public string Name {get;set}
}

public class Person {
   public string FirstName {get;set}
   public string SecondName {get;set}
   public string Name => $"{FirstName} {SecondName}";
}

,

public class Person {
   public string FirstName {get;set}
   public string SecondName {get;set}
   public string Name => $"{FirstName} {SecondName}";

   public void migrate_1(JToken token, JsonSerializer s){
      var name = token["Name"];
      var names = names.Split(" ");
      token["FirstName"] = names[0];
      token["SecondName"] = names[1];
      return token;
   }
}

, . . 13 , .

0

Source: https://habr.com/ru/post/1691903/


All Articles