Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to apply ObjectCreationHandling.Replace to selected properties when deserializing JSON?

I have a class that contains a List<Tuple<int, int, int>> property whose default constructor allocates the list and fills it with some default values, for instance:

public class Configuration
{
    public List<Tuple<int, int, int>> MyThreeTuple { get; set; }

    public Configuration()
    {
        MyThreeTuple = new List<Tuple<int, int, int>>();
        MyThreeTuple.Add(new Tuple<int, int, int>(-100, 20, 501));
        MyThreeTuple.Add(new Tuple<int, int, int>(100, 20, 864));
        MyThreeTuple.Add(new Tuple<int, int, int>(500, 20, 1286));
    }
}

When I deserialize an instance of this class from JSON using Json.NET, the values from JSON get added to the list rather than replacing the items in the list, causing the list to have too many values. A solution to this problem is given in Json.Net calls property getter during deserialization of list, resulting in duplicate items.

var settings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace };
var config = JsonConvert.DeserializeObject<Configuration>(jsonString, settings);    

This causes Json.NET to allocate fresh instances everything being deserialized.

However, this introduces an additional problem: my class exists in a larger object graph, and some of the types in the graph do not have default constructors. They are instead constructed by a constructor in the containing class. If I use ObjectCreationHandling = ObjectCreationHandling.Replace, Json.NET fails trying to construct instances of these types with the following exception:

Unable to find a constructor to use for the type MySpecialType. A class 
should either have a default constructor, one constructor with arguments
or a constructor marked with the JsonConstructor attribute. 

How can I apply ObjectCreationHandling.Replace selectively to certain properties in my object graph, and not others?

like image 648
KDecker Avatar asked Nov 16 '15 20:11

KDecker


1 Answers

You have a few alternatives to force your list to be replaced rather than reused:

  1. You can add an attribute to the list property indicating that it should be replaced not reused:

    public class Configuration
    {
        [JsonProperty(ObjectCreationHandling = ObjectCreationHandling.Replace)]
        public List<Tuple<int, int, int>> MyThreeTuple { get; set; }
    }
    
  2. You can use an array instead of a list, as arrays are always replaced. This might make sense if your list should always contain three items and is never resized:

    public class Configuration
    {
        public Tuple<int, int, int>[] MyThreeTuple { get; set; }
    
        public Configuration()
        {
            MyThreeTuple = new[]
            {
                new Tuple<int, int, int>(-100, 20, 501),
                new Tuple<int, int, int>(100, 20, 864),
                new Tuple<int, int, int>(500, 20, 1286),
            };
        }
    }
    
  3. If you don't want your class definitions to have a dependency on Json.NET, you can make a custom JsonConverter that clears the list when deserializing:

    public class ConfigurationConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType)
        {
            return typeof(Configuration).IsAssignableFrom(objectType);
        }
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            var config = (existingValue as Configuration ?? (Configuration)serializer.ContractResolver.ResolveContract(objectType).DefaultCreator());
            if (config.MyThreeTuple != null)
                config.MyThreeTuple.Clear();
            serializer.Populate(reader, config);
            return config;
        }
    
        public override bool CanWrite { get { return false; } }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    

    Then use it with the following JsonSerializerSettings:

    var settings = new JsonSerializerSettings { Converters = new JsonConverter[] { new ConfigurationConverter() } };
    
  4. If you want all list properties to be replaced rather than reused, you can make a custom ContractResolver that does this:

    public class ListReplacementContractResolver : 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."
        static readonly ListReplacementContractResolver instance;
    
        // Using a static constructor enables fairly lazy initialization.  http://csharpindepth.com/Articles/General/Singleton.aspx
        static ListReplacementContractResolver() { instance = new ListReplacementContractResolver(); }
    
        public static ListReplacementContractResolver Instance { get { return instance; } }
    
        protected ListReplacementContractResolver() : base() { }
    
        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
        {
            var jsonProperty = base.CreateProperty(member, memberSerialization);
            if (jsonProperty.ObjectCreationHandling == null && jsonProperty.PropertyType.GetListType() != null)
                jsonProperty.ObjectCreationHandling = ObjectCreationHandling.Replace;
            return jsonProperty;
        }
    }
    
    public static class TypeExtensions
    {
        public static Type GetListType(this Type type)
        {
            while (type != null)
            {
                if (type.IsGenericType)
                {
                    var genType = type.GetGenericTypeDefinition();
                    if (genType == typeof(List<>))
                        return type.GetGenericArguments()[0];
                }
                type = type.BaseType;
            }
            return null;
        }
    }
    

    Then use it with the following settings:

    var settings = new JsonSerializerSettings { ContractResolver = ListReplacementContractResolver.Instance };
    
  5. If the collection is get-only (which it is not in this case) see Clear collections before adding items when populating existing objects.

like image 91
dbc Avatar answered Nov 14 '22 07:11

dbc