3

I have a custom type derived from the DynamicObject type. This type has fixed properties declared in the type. So it allows the user to provide some required properties in addition to any dynamic properties they want. When I use the JsonConvert.DeserializeObject<MyType>(json) method to deserialize the data for this type, it does not set the declared properties, but those properties are accessible via the object indexer property on the dynamic object. This tells me that it simply treats the object as a dictionary and does not try to call the declared property setters nor is it using them for inferring the property type information.

Has anyone encountered this situation before? Any idea how I can instruct the JsonConvert class to take the declared properties into account when deserializing the object data?

I tried to use a custom JsonConverter, but that requires me to write the complex JSON read and writ methods. I was hoping to find a way inject property contract information by overriding the JsonContractResolver or JsonConverter, etc.


//#define IMPLEMENT_IDICTIONARY

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

namespace ConsoleApp1
{
    class Program
    {
        public class MyDynamicObject : DynamicObject
#if IMPLEMENT_IDICTIONARY
            , IDictionary<string, object>
#endif
        {
            private Dictionary<string, object> m_Members;

            public MyDynamicObject()
            {
                this.m_Members = new Dictionary<string, object>();
            }


#if IMPLEMENT_IDICTIONARY
            public int Count { get { return this.m_Members.Count; } }

            public ICollection<string> Keys => this.m_Members.Keys;

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

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

            /// <summary>
            /// Gets or sets the specified member value.
            /// </summary>
            /// <param name="memberName">Name of the member in question.</param>
            /// <returns>A value for the specified member.</returns>
            public object this[string memberName]
            {
                get
                {
                    object value;
                    if (this.m_Members.TryGetValue(memberName, out value))
                        return value;
                    else
                        return null;
                }
                set => this.m_Members[memberName] = value;
            }
#endif


            public override bool TryGetMember(GetMemberBinder binder, out object result)
            {
                this.m_Members.TryGetValue(binder.Name, out result);
                return true;
            }

            public override bool TrySetMember(SetMemberBinder binder, object value)
            {
                this.m_Members[binder.Name] = value;
                return true;
            }

            public override bool TryDeleteMember(DeleteMemberBinder binder)
            {
                return this.m_Members.Remove(binder.Name);
            }

            public override IEnumerable<string> GetDynamicMemberNames()
            {
                var names = base.GetDynamicMemberNames();
                return this.m_Members.Keys;
            }

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

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

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

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

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

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

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

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

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

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

            IEnumerator IEnumerable.GetEnumerator()
            {
                return this.m_Members.GetEnumerator();
            }
#endif
        }

        public class ProxyInfo
        {
            public string Server;
            public int Port;
        }

        public class CustomDynamicObject : MyDynamicObject
        {
            //[JsonProperty] // NOTE: Cannot do this.
            public string Name { get; set; }

            //[JsonProperty]  // NOTE: Cannot do this.
            public ProxyInfo Proxy { get; set; }
        }


        static void Main(string[] args)
        {
            dynamic obj = new CustomDynamicObject()
            {
                Name = "Test1",
                Proxy = new ProxyInfo() { Server = "http://test.com/",  Port = 10102 }
            };
            obj.Prop1 = "P1";
            obj.Prop2 = 320;

            string json = JsonConvert.SerializeObject(obj);  // Returns: { "Prop1":"P1", "Prop2":320 }

            // ISSUE #1: It did not serialize the declared properties. Only the dynamically added properties are serialized.
            //           Following JSON was expected. It produces correct JSON if I mark the declared properties with
            //           JsonProperty attribute, which I cannot do in all cases.
            string expectedJson = "{ \"Prop1\":\"P1\", \"Prop2\":320, \"Name\":\"Test1\", \"Proxy\":{ \"Server\":\"http://test.com/\", \"Port\":10102 } }";


            CustomDynamicObject deserializedObj = JsonConvert.DeserializeObject<CustomDynamicObject>(expectedJson);

            // ISSUE #2: Deserialization worked in this case, but does not work once I re-introduce the IDictionary interface on my base class.
            //           In that case, it does not populate the declared properties, but simply added all 4 properties to the underlying dictionary.
            //           Neither does it infer the ProxyInfo type when deserializing the Proxy property value and simply bound the JObject token to
            //           the dynamic object.
        }
    }
}

I would have expected it to use reflection to resolve the property and its type information like it does for regular types. But it seems as if it simply treats the object as a regular dictionary.

Note that:

  • I cannot remove the IDictionary<string, object> interface since some of the use-cases in my API rely on the object to be a dictionary, not dynamic.

  • Adding [JsonProperty] to all declared properties to be serialized is not practical because its derived types are created by other developers and they do not need to care about the persistence mechanism explicitly.

Any suggestions on how I can make it work correctly?

dbc
  • 104,963
  • 20
  • 228
  • 340
B Singh
  • 113
  • 1
  • 9
  • Can you share a [mcve] that includes your implementation of `MyDynamicObject`? This should work as long as `MyDynamicObject ` is implemented correctly. See e.g. [Serialize instance of a class deriving from DynamicObject class](https://stackoverflow.com/q/49118571/3744182) where the problem was a failure to override `GetDynamicMemberNames()`. You may need to mark your serializable properties with `[JsonProperty]`, see [C# How to serialize (JSON, XML) normal properties on a class that inherits from DynamicObject](https://stackoverflow.com/a/18822202/3744182). – dbc Apr 02 '19 at 04:08
  • Thanks for the quick response. I had two problems with my custom type: 1) It did not implement `GetDynamicMemberNames()` method, and 2) it implemented the `IDictionary` interface. When I removed the `IDictionary` interface and implemented the `GetDynamicMemberNames` method, the serialization did work. – B Singh Apr 02 '19 at 15:27
  • However, the declared properties require the `[JsonProperty]` attribute, which I cannot do in all cases since these derived types are created by other developers and they do not need to care about the persistence mechanism explicitly. I can ask them to change their libraries to include the attribute, but I would like to see if this can be handled automatically in my base library code. Any suggestions on that? – B Singh Apr 02 '19 at 15:28
  • I actual need to have my custom type implement the IDictionary interface since some of the use-cases in my API reply on the object to be a dictionary, not dynamic. Any suggestions on how to keep the IDictionary interface and still have the serialization/deserialization work properly? – B Singh Apr 02 '19 at 15:29
  • Thanks. Updated the code sample above with the working one. – B Singh Apr 02 '19 at 17:26

2 Answers2

2

You have a few problems here:

  1. You need to correctly override DynamicObject.GetDynamicMemberNames() as explained in this answer to Serialize instance of a class deriving from DynamicObject class by AlbertK for Json.NET to be able to serialize your dynamic properties.

    (This has already been fixed in the edited version of your question.)

  2. Declared properties do not show up unless you explicitly mark them with [JsonProperty] (as explained in this answer to C# How to serialize (JSON, XML) normal properties on a class that inherits from DynamicObject) but your type definitions are read-only and cannot be modified.

    The problem here seems to be that JsonSerializerInternalWriter.SerializeDynamic() only serializes declared properties for which JsonProperty.HasMemberAttribute == true. (I don't know why this check is made there, it would seem to make more sense to set CanRead or Ignored inside the contract resolver.)

  3. You would like for your class to implement IDictionary<string, object>, but if you do, it breaks deserialization; declared properties are no longer populated, but are instead added to the dictionary.

    The problem here seems to be that DefaultContractResolver.CreateContract() returns JsonDictionaryContract rather than JsonDynamicContract when the incoming type implements IDictionary<TKey, TValue> for any TKey and TValue.

Assuming you have fixed issue #1, issues #2 and #3 can be handled by using a custom contract resolver such as the following:

public class MyContractResolver : DefaultContractResolver
{
    protected override JsonContract CreateContract(Type objectType)
    {
        // Prefer JsonDynamicContract for MyDynamicObject
        if (typeof(MyDynamicObject).IsAssignableFrom(objectType))
        {
            return CreateDynamicContract(objectType);
        }
        return base.CreateContract(objectType);
    }

    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
        var properties = base.CreateProperties(type, memberSerialization);
        // If object type is a subclass of MyDynamicObject and the property is declared
        // in a subclass of MyDynamicObject, assume it is marked with JsonProperty 
        // (unless it is explicitly ignored).  By checking IsSubclassOf we ensure that 
        // "bookkeeping" properties like Count, Keys and Values are not serialized.
        if (type.IsSubclassOf(typeof(MyDynamicObject)) && memberSerialization == MemberSerialization.OptOut)
        {
            foreach (var property in properties)
            {
                if (!property.Ignored && property.DeclaringType.IsSubclassOf(typeof(MyDynamicObject)))
                {
                    property.HasMemberAttribute = true;
                }
            }
        }
        return properties;
    }
}

Then, to use the contract resolver, cache it somewhere for performance:

static IContractResolver resolver = new MyContractResolver();

And then do:

var settings = new JsonSerializerSettings
{
    ContractResolver = resolver,
};
string json = JsonConvert.SerializeObject(obj, settings);

Sample fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • With the extended contract resolver as described above, now I am getting another issue! If my JSON file contains a comment, it fails to deserialize the file. Throws `Unexpected token when deserializing object: Comment. Path 'appBaseUrl', line 4, position 125.`. That property in JSON file looks like `"appBaseUrl": "http://wwww.WillBeReplacedInCode.com", // NOTE: Placeholder for the runtime location to be used by the test.`. I do not get this issue when I don't set the contract resolver in the settings object. Any ideas why that would fail? – B Singh Apr 03 '19 at 20:28
  • @BSingh - I would need to see a [mcve]. Note that [comments are not part of the JSON standard](https://stackoverflow.com/q/244777), they are an extension that Json.NET supports. And, their support sometimes has bugs, see e.g. https://github.com/JamesNK/Newtonsoft.Json/issues/1545. – dbc Apr 03 '19 at 20:36
  • @BSingh - Possibly [`CreateDynamic()`](https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs#L1803) has a bug with comments whereas [`PopulateDictionary()`](https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs#L1337) does not. – dbc Apr 03 '19 at 20:39
  • Yes. Thanks. There seems to be another bug in dynamic object contract resolver. It does not populate the values of declared properties of type list or dictionary unless the property includes a setter. For regular types, it works fine without a property setter for such properties. – B Singh Apr 05 '19 at 12:45
0

I can't tell what is inside of ProxyInfo class. However, when using a string for both Name and Proxy property, deserialization works correctly. Please check the following working sample:

    class Program
    {
        static void Main(string[] args)
        {
            // NOTE: This is how I load the JSON data into the new type.
            var obj = JsonConvert.DeserializeObject<MyCustomDynamicObject>("{name:'name1', proxy:'string'}");
            var proxy = obj.Proxy;
            var name = obj.Name;
        }
    }

    public class MyDynamicObject : DynamicObject
    {
        // Implements the functionality to store dynamic properties in 
        // dictionary.
        // NOTE: This base class does not have any declared properties.
    }

    // NOTE: This is the actual concrete type that has declared properties
    public class MyCustomDynamicObject : MyDynamicObject
    {
        public string Name { get; set; }
        public string Proxy { get; set; }
    }
Gru b.
  • 1
  • 2