Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
481 views
in Technique[技术] by (71.8m points)

c# - 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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...