0

I have a custom type that derives from a DynamicObject and implements the IDictionary<string, object> interface. In order to make this setup work with Json.NET, I use a custom contract resolver (here is a post where I talked about this usecase). When I use the SerializeObject<T> method, it is able to deserialize the object from JSON without any issues, but when I create the object first and then use the PopulateObject method instead, it fails with the following error.

Newtonsoft.Json.JsonSerializationException: 'Cannot populate JSON object onto type 'ConsoleApp5.Program+Settings'. Path '$schema', line 1, position 12.'

Here is my sample code that demonstrates the issue:

using System;
using System.Collections.Generic;
using System.Dynamic;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

namespace ConsoleApp5
{
    class Program
    {
        /// <summary>
        /// This is a custom contract resolver I am using to support DynamicObject types that also implement IDictionary
        /// interface.
        /// </summary>
        public class DefaultJsonContractResolver : DefaultContractResolver
        {
            protected override JsonContract CreateContract(Type objectType)
            {
                // FIX: ElasticObject is a DynamicObject, but it also implements the IDictionary<string, object> interface.
                //      When the DefaultContractResolver sees the type implementing the IDictionary<string, object> interface,
                //      it eagerly uses the dictionary type contract instead of dynamic type contract. This causes the JSON
                //      deserialization to only output the dynamically added properties and ignore the declared ones. The
                //      following code fixes this problem.
                if (typeof(ElasticObject).IsAssignableFrom(objectType))
                {
                    var contract = CreateDynamicContract(objectType);
                    return contract;
                }

                return base.CreateContract(objectType);
            }

            protected override IList<JsonProperty> CreateProperties(Type objectType, MemberSerialization memberSerialization)
            {
                IList<JsonProperty> properties = base.CreateProperties(objectType, memberSerialization);

                // FIX: ElasticObject is a DynamicObject. But the dynamic object JSON contract type used by the Json.NET library
                //      does not handle the declared properties on dynamic objects, unless those properties are decorated with
                //      the JsonProperty attribute. The following code fixes this problem by automatically including those
                //      declared properties into account.
                Type elasticObjectType = typeof(ElasticObject);
                // NOTE: ElasticObject itself does not have any declared properties, except for the dictionary interface ones
                //       that do not need to be serialized and deserialized. So only do the automatic inclusion for the properties
                //       that are declared in the types derived from the ElasticObject.
                if ((memberSerialization == MemberSerialization.OptOut) && objectType.IsSubclassOf(elasticObjectType))
                {
                    foreach (JsonProperty property in properties)
                    {
                        if (!property.Ignored && property.DeclaringType.IsSubclassOf(elasticObjectType))
                            property.HasMemberAttribute = true; // Assume the declared properties are marked with JsonProperty (unless it is explicitly ignored).
                    }
                }

                return properties;
            }
        }

        /// <summary>
        /// This is the base type that I use for custom type in question below.
        /// </summary>
        public class ElasticObject : DynamicObject, IDictionary<string, object>
        {
            private Dictionary<string, object> m_Members;
            private bool m_IgnoreCase;
            private bool m_IsNullObject;

            public ElasticObject()
            {
                m_Members = new Dictionary<string, object>();
            }

            public ElasticObject(int capacity, bool ignoreCase = false)
            {
                m_IgnoreCase = ignoreCase;

                if (capacity > 0)
                {
                    if (ignoreCase)
                        m_Members = new Dictionary<string, object>(capacity, StringComparer.OrdinalIgnoreCase);
                    else
                        m_Members = new Dictionary<string, object>(capacity);
                }
                else if (ignoreCase)
                {
                    m_Members = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
                }
                else
                {
                    m_Members = new Dictionary<string, object>();
                }
            }


            public int Count
            {
                get { return m_Members.Count; }
            }

            public bool IsEmpty
            {
                get { return Count == 0; }
            }

            public bool ForMissingMember
            {
                get { return m_IsNullObject; }
            }

            #region IDictionary properties...
            public ICollection<string> Keys => m_Members.Keys;

            public ICollection<object> Values => m_Members.Values;

            bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false;

            public object this[string memberName]
            {
                get
                {
                    object value;
                    if (m_Members.TryGetValue(memberName, out value))
                        return value;
                    else
                        return null;
                }
                set => m_Members[memberName] = value;
            }
            #endregion


            public bool HasMember(string memberName)
            {
                return m_Members.ContainsKey(memberName);
            }

            public override IEnumerable<string> GetDynamicMemberNames()
            {
                if (m_Members != null)
                    return m_Members.Keys;
                else
                    return base.GetDynamicMemberNames();
            }

            public override bool TryGetMember(GetMemberBinder binder, out object result)
            {
                // NOTE: The binder object does not seem to provide the expected return type where it is known (like in an assignment expression).
                //       But the code below is still written to expect that.
                Type returnType = binder.ReturnType;

                string memberName = GetMemberNameKey(binder.Name, binder.IgnoreCase);
                if (m_Members.TryGetValue(memberName, out result))
                {
                    if (returnType != typeof(object) && result != null && returnType.IsAssignableFrom(result.GetType()))
                        result = Convert.ChangeType(result, returnType);
                }
                else  // New member encountered that has not be assigned a value yet.
                {
                    if (returnType == typeof(object)
                        || returnType == typeof(ElasticObject)
                        || returnType == typeof(IDynamicMetaObjectProvider))
                    {
                        ElasticObject nullObj = new ElasticObject(0, m_IgnoreCase);
                        nullObj.m_IsNullObject = true;

                        m_Members[memberName] = result = nullObj;
                    }
                    else if (returnType == typeof(ExpandoObject))
                    {
                        m_Members[memberName] = result = new ExpandoObject();
                    }
                    else
                    {
                        result = returnType.GetDefaultTypeValue();
                    }
                }
                return true;
            }

            public override bool TrySetMember(SetMemberBinder binder, object value)
            {
                string memberName = GetMemberNameKey(binder.Name, binder.IgnoreCase);
                m_Members[memberName] = value;

                m_IsNullObject = false;

                return true;
            }

            public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
            {
                string memberName = GetMemberNameKey(binder.Name, binder.IgnoreCase);
                object method;
                if (m_Members.TryGetValue(memberName, out method))
                {
                    if (method is Delegate)
                    {
                        result = ((Delegate)method).DynamicInvoke(args);
                        return true;
                    }
                }

                return base.TryInvokeMember(binder, args, out result);
            }

            public override bool TryDeleteMember(DeleteMemberBinder binder)
            {
                string memberName = GetMemberNameKey(binder.Name, binder.IgnoreCase);
                return m_Members.Remove(memberName);
            }

            public override bool TryConvert(ConvertBinder binder, out object result)
            {
                if (m_Members.Count != 0)
                {
                    result = null;
                    return false;
                }
                else
                {
                    result = binder.Type.GetDefaultTypeValue();
                    return true;
                }
            }

            private string GetMemberNameKey(string name, bool ignoreCase)
            {
                // If the object does not ignore case but the DLR platform is asking to ignore it, then
                if (!m_IgnoreCase && ignoreCase)
                {
                    // Find the actual member name that was used.
                    foreach (string key in m_Members.Keys)
                    {
                        if (name.Equals(key, StringComparison.OrdinalIgnoreCase))
                            return key;
                    }
                }

                return name;
            }

            #region IDictionary members...

            bool IDictionary<string, object>.ContainsKey(string memberName)
            {
                return m_Members.ContainsKey(memberName);
            }

            public void Add(string memberName, object value)
            {
                m_Members.Add(memberName, value);
            }

            public bool Remove(string memberName)
            {
                return m_Members.Remove(memberName);
            }

            public bool TryGetValue(string memberName, out object value)
            {
                return m_Members.TryGetValue(memberName, out value);
            }

            public void Clear()
            {
                m_Members.Clear();
            }

            #region ICollection members...
            void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> member)
            {
                ((IDictionary<string, object>)m_Members).Add(member);
            }

            bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> member)
            {
                return ((IDictionary<string, object>)m_Members).Contains(member);
            }

            public void CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
            {
                ((IDictionary<string, object>)m_Members).CopyTo(array, arrayIndex);
            }

            bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> member)
            {
                return ((IDictionary<string, object>)m_Members).Remove(member);
            }
            #endregion

            public IEnumerator<KeyValuePair<string, object>> GetEnumerator()
            {
                return m_Members.GetEnumerator();
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return ((System.Collections.IEnumerable)m_Members).GetEnumerator();
            }
            #endregion
        }


        public class Settings : ElasticObject
        {
            public string Name { get; set; }
        }


        static void Main(string[] args)
        {
            string json = "{ '$schema': 'http://test.schema', 'name': 'Some name'}";

            JsonSerializerSettings jsonSettings = new JsonSerializerSettings()
            {
                CheckAdditionalContent = false,
                DefaultValueHandling = DefaultValueHandling.Populate,
                MissingMemberHandling = MissingMemberHandling.Ignore,
                NullValueHandling = NullValueHandling.Ignore,
                ObjectCreationHandling = ObjectCreationHandling.Auto,
                ContractResolver = new DefaultJsonContractResolver()    // Custom contract resolver
            };

            // Works
            Settings obj = JsonConvert.DeserializeObject<Settings>(json, jsonSettings);

            // Does not work
            obj = new Settings();
            JsonConvert.PopulateObject(json, obj, jsonSettings);
        }
    }

    public static class TypeHelpers
    {
        public static object GetDefaultTypeValue(this Type targetType)
        {
            if (targetType == null)
                throw new ArgumentNullException("targetType");

            if (targetType.IsValueType)
                return Activator.CreateInstance(targetType);
            return null;
        }
    }
}

Can someone help me figure out why I cannot use the PopulateObject method in this case or what I need to change to make it work?

B Singh
  • 113
  • 1
  • 9
  • Demo with traceback here: https://dotnetfiddle.net/ENehs5. It looks like [`JsonSerializerInternalReader.Populate(JsonReader reader, Object target)`](https://github.com/JamesNK/Newtonsoft.Json/blob/52e257ee57899296d81a868b32300f0b3cfeacbe/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs#L2336) simply doesn't implement populating of objects with `JsonDynamicContract` contracts. ... – dbc Aug 09 '21 at 16:07
  • If we look at [`CreateObject()`](https://github.com/JamesNK/Newtonsoft.Json/blob/52e257ee57899296d81a868b32300f0b3cfeacbe/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs#L427), for `JsonDynamicContract` it calls [`CreateDynamic()`](https://github.com/JamesNK/Newtonsoft.Json/blob/52e257ee57899296d81a868b32300f0b3cfeacbe/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs#L1824) which handles both creation and population in one method. Seems like you will need to implement your own extension method to do it unfortunately. – dbc Aug 09 '21 at 16:10
  • I tried using the `CustomCreationConverter` type as well, but that did not help either. How do you recommend that I make it work with some override type or are you suggesting that I create that whole logic in the `CreateDynamic()` outside in my own code? – B Singh Aug 09 '21 at 19:42
  • Custom converters aren't called for the root object when populating it, so using `CustomCreationConverter` won't help. *are you suggesting that I create that whole logic in the `CreateDynamic()` outside in my own code?* -- that's the only workaround I see. Do you want an answer that shows how to do that? (It might take a while) You could also try opening an issue with Newtonsoft. – dbc Aug 09 '21 at 19:51
  • Thanks. Appreciate your response. I will log this as an issue with Newtonsoft and will try to the solution you suggested as well in the meantime. – B Singh Aug 10 '21 at 20:48

0 Answers0