具有DynamicObject和TypeCreationConverter的JsonConvert.DeserializeObject

时间:2018-04-19 08:21:37

标签: c# json.net dynamicobject

我有一个类EntityBase,它派生自DynamicObject而没有空的默认构造函数。

// this is not the actual type but a mock to test the behavior with
public class EntityBase : DynamicObject
{
    public string EntityName { get; private set; }

    private readonly Dictionary<string, object> values = new Dictionary<string, object>();

    public EntityBase(string entityName)
    {
        this.EntityName = entityName;
    }

    public virtual object this[string fieldname]
    {
        get
        {
            if (this.values.ContainsKey(fieldname))
                return this.values[fieldname];
            return null;
        }
        set
        {
            if (this.values.ContainsKey(fieldname))
                this.values[fieldname] = value;
            else
                this.values.Add(fieldname, value);          
        }
    }

    public override IEnumerable<string> GetDynamicMemberNames()
    {
        return this.values.Keys.ToList();
    }

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

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

JSON我想反序列化看起来像这样:

{'Name': 'my first story', 'ToldByUserId': 255 }

EntityBase既没有Name也没有ToldByUserId属性。它们应该添加到DynamicObject。

如果我让DeserializeObject像这样创建对象,一切都按预期工作:

var story = JsonConvert.DeserializeObject<EntityBase>(JSON);

但由于我没有一个空的默认构造函数而且无法更改我为CustomCreationConverter而去的课程:

public class StoryCreator : CustomCreationConverter<EntityBase>
{
    public override EntityBase Create(Type objectType)
    {
        return new EntityBase("Story");
    }
}

但是

var stroy = JsonConvert.DeserializeObject<EntityBase>(JSON, new StoryCreator());

引发

  

无法将JSON对象填充到类型&#39; DynamicObjectJson.EntityBase&#39;上。   路径&#39;名称&#39;,第1行,第8位。

似乎DeserializeObject调用PopulateObject创建的对象CustomCreationConverter。当我尝试手动执行此操作时,错误保持不变

JsonConvert.PopulateObject(JSON, new EntityBase("Story"));

我进一步假设PopulateObject不会检查目标类型是否来自DynamicObject,因此不会回退到TrySetMember

请注意,我对EntityBase类型定义没有影响,它来自外部库并且无法更改。

任何见解都将受到高度赞赏!

修改:添加了一个示例:https://dotnetfiddle.net/EGOCFU

1 个答案:

答案 0 :(得分:1)

您似乎偶然发现Json.NET支持反序列化动态对象(定义为生成JsonDynamicContract的对象)的一些错误或限制:

  1. 不存在对参数化构造函数的支持。即使其中一个标有[JsonConstructor],也不会被使用。

    此处JsonSerializerInternalReader.CreateDynamic()似乎完全没有预加载所有属性的必要逻辑。与JsonSerializerInternalReader.CreateNewObject()比较,表明需要什么。

    由于逻辑看起来相当精细,这可能是一个限制而不是一个错误。实际上有closed issue #47表示它没有实现:

      

    添加此功能会有相当多的工作。如果您添加拉取请求,欢迎提交拉取请求。

  2. Json.NET无法填充预先存在的动态对象。与常规对象(生成JsonObjectContract的对象)不同,构造和填充的逻辑完全包含在前面提到的JsonSerializerInternalReader.CreateDynamic()中。

    我不明白为什么用相当简单的代码重构无法实现这一点。你可以submit an issue要求这个。如果实施此操作,您的StoryCreator将按原样运作。

  3. 在没有#1或#2的情况下,可以创建一个custom JsonConverter,其逻辑大致在JsonSerializerInternalReader.CreateDynamic()上建模,调用指定的创建方法然后填充动态和非动态属性,像这样:

    public class EntityBaseConverter : ParameterizedDynamicObjectConverterBase<EntityBase>
    {
        public override EntityBase CreateObject(JObject jObj, Type objectType, JsonSerializer serializer, ICollection<string> usedParameters)
        {
            var entityName = jObj.GetValue("EntityName", StringComparison.OrdinalIgnoreCase);
            if (entityName != null)
            {
                usedParameters.Add(((JProperty)entityName.Parent).Name);
            }
            var entityNameString = entityName == null ? "" : entityName.ToString();
            if (objectType == typeof(EntityBase))
            {
                return new EntityBase(entityName == null ? "" : entityName.ToString());             
            }
            else
            {
                return (EntityBase)Activator.CreateInstance(objectType, new object [] { entityNameString });
            }           
        }
    }
    
    public abstract class ParameterizedDynamicObjectConverterBase<T> : JsonConverter where T : DynamicObject
    {
        public override bool CanConvert(Type objectType) { return typeof(T).IsAssignableFrom(objectType); } // Or possibly return objectType == typeof(T);
    
        public abstract T CreateObject(JObject jObj, Type objectType, JsonSerializer serializer, ICollection<string> usedParameters);
    
        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            // Logic adapted from JsonSerializerInternalReader.CreateDynamic()
            // https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/JsonSerializerInternalReader.cs#L1751
            // By James Newton-King https://github.com/JamesNK
    
            var contract = (JsonDynamicContract)serializer.ContractResolver.ResolveContract(objectType);
    
            if (reader.TokenType == JsonToken.Null)
                return null;
    
            var jObj = JObject.Load(reader);
    
            var used = new HashSet<string>();
            var obj = CreateObject(jObj, objectType, serializer, used);
    
            foreach (var jProperty in jObj.Properties())
            {
                var memberName = jProperty.Name;
                if (used.Contains(memberName))
                    continue;
                // first attempt to find a settable property, otherwise fall back to a dynamic set without type
                JsonProperty property = contract.Properties.GetClosestMatchProperty(memberName);
    
                if (property != null && property.Writable && !property.Ignored)
                {
                    var propertyValue = jProperty.Value.ToObject(property.PropertyType, serializer);
                    property.ValueProvider.SetValue(obj, propertyValue);
                }
                else
                {
                    object propertyValue;
                    if (jProperty.Value.Type == JTokenType.Null)
                        propertyValue = null;
                    else if (jProperty.Value is JValue)
                        // Primitive
                        propertyValue = ((JValue)jProperty.Value).Value;
                    else
                        propertyValue = jProperty.Value.ToObject<IDynamicMetaObjectProvider>(serializer);
                    // Unfortunately the following is not public!
                    // contract.TrySetMember(obj, memberName, propertyValue);
                    // So we have to duplicate the logic of what Json.NET has already done.
                    CallSiteCache.SetValue(memberName, obj, propertyValue);
                }               
            }
            return obj;
        }
    
        public override bool CanWrite { get { return false; } }
    
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            throw new NotImplementedException();
        }
    }
    
    internal static class CallSiteCache
    {
        // Adapted from the answer to 
        // https://stackoverflow.com/questions/12057516/c-sharp-dynamicobject-dynamic-properties
        // by jbtule, https://stackoverflow.com/users/637783/jbtule
        // And also
        // https://github.com/mgravell/fast-member/blob/master/FastMember/CallSiteCache.cs
        // by Marc Gravell, https://github.com/mgravell
    
        private static readonly Dictionary<string, CallSite<Func<CallSite, object, object, object>>> setters 
            = new Dictionary<string, CallSite<Func<CallSite, object, object, object>>>();
    
        public static void SetValue(string propertyName, object target, object value)
        {
            CallSite<Func<CallSite, object, object, object>> site;
    
            lock (setters)
            {
                if (!setters.TryGetValue(propertyName, out site))
                {
                    var binder = Binder.SetMember(CSharpBinderFlags.None,
                           propertyName, typeof(CallSiteCache),
                           new List<CSharpArgumentInfo>{
                                   CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
                                   CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)});
                    setters[propertyName] = site = CallSite<Func<CallSite, object, object, object>>.Create(binder);
                }
            }
    
            site.Target(site, target, value);
        }
    }
    

    然后使用它:

    var settings = new JsonSerializerSettings
    {
        Converters = { new EntityBaseConverter() },
    };
    var stroy = JsonConvert.DeserializeObject<EntityBase>(JSON, settings);
    

    因为似乎EntityBase可能是多个派生类的基类,所以我编写了转换器以适用于EntityBase的所有派生类型,并假设它们都具有参数化构造函数相同的签名。

    注意我从JSON中获取EntityName。如果您希望将其硬编码为"Story",则可以执行此操作,但您仍应将EntityName属性的实际名称添加到usedParameters集合,以防止具有相同的动态属性来自创建的名称。

    示例工作.Net小提琴here