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?
You have a few problems here:
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.)
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.)
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.