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?