带有DynamicObject和TypeCreationConverter的JsonConvert.DeserializeObject [英] JsonConvert.DeserializeObject w/ DynamicObject and TypeCreationConverter

查看:67
本文介绍了带有DynamicObject和TypeCreationConverter的JsonConvert.DeserializeObject的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个 EntityBase 类,它是从 DynamicObject 派生的,没有空的默认构造函数.

I have a class EntityBase that derives from DynamicObject without an empty default constructor.

// 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看起来像这样:

the JSON I'd like to deserialize looks like this:

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

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

EntityBase has neither the Name nor the ToldByUserId property. They should be added to the DynamicObject.

如果我让 DeserializeObject 创建这样的对象,一切都会按预期进行:

If I let DeserializeObject create the object like this everything works as expected:

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

但是因为我没有一个空的默认构造函数,并且无法更改该类,所以我选择了 CustomCreationConverter :

but since I don't have an empty default constructor and can't change the class I went for a 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对象填充到类型"DynamicObjectJson.EntityBase"上.路径名称",第1行,位置8.

Cannot populate JSON object onto type 'DynamicObjectJson.EntityBase'. Path 'Name', line 1, position 8.

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

It seems that the DeserializeObject calls PopulateObject on the object that was created by the CustomCreationConverter. When I try to do this manually the error stays the same

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

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

I further assume that PopulateObject does not check if the target type derives from DynamicObject and therefore does not fall back to TrySetMember.

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

Note that I don't have influence on the EntityBase type definition, it's from an external library and cannot be changed.

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

Any insights would be highly appreciated!

编辑:添加了一个示例: https://dotnetfiddle.net/EGOCFU

Edit: added an example: https://dotnetfiddle.net/EGOCFU

推荐答案

您似乎偶然发现了Json.NET支持反序列化动态对象(定义为

You seem to have stumbled on a couple of bugs or limitations in Json.NET's support for deserializing dynamic objects (defined as those for which a JsonDynamicContract is generated):

  1. 不存在对参数化构造函数的支持.即使使用 [JsonConstructor] 标记它,也不会被使用.

  1. Support for parameterized constructors is not present. Even if one is marked with [JsonConstructor] it will not get used.

在这里,预加载所有属性的必要逻辑似乎从 JsonSerializerInternalReader比较.CreateNewObject() ,指示需要什么.

Here the necessary logic to pre-load all the properties seems to be entirely missing from JsonSerializerInternalReader.CreateDynamic(). Compare with JsonSerializerInternalReader.CreateNewObject() which indicates what would be required.

由于逻辑看起来很复杂,所以这可能是一个限制,而不是一个错误.实际上,存在已关闭问题#47 ,表明该问题尚未实现:

Since the logic looks fairly elaborate this might be a limitation rather than a bug. And actually there is closed issue #47 about this indicating that it's not implemented:

要添加此功能,需要做很多工作.欢迎您添加拉取请求.

There would be a fair bit of work to add this feature. You are welcome to submit a pull request if you do add it.

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

    我不明白为什么无法通过相当简单的代码重组来实现这一点.您可能提交问题要求这样做.如果实施了此方法,您的 StoryCreator 将会照常运行.

    I don't see why this couldn't be implemented with a fairly simple code restructuring. You might submit an issue asking for this. If this were implemented, your StoryCreator would work as-is.

    在没有#1或#2的情况下,可以创建自定义 JsonConverter ,其逻辑大致基于 JsonSerializerInternalReader.CreateDynamic()进行建模,该方法调用指定的创建方法,然后填充动态和非动态属性,如下所示:

    In the absence of either #1 or #2, it's possible to create a custom JsonConverter whose logic is modeled roughly on JsonSerializerInternalReader.CreateDynamic() which calls a specified creation method then populates both dynamic and non-dynamic properties, like so:

    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 的所有派生类型,并假设它们都具有相同签名的参数化构造函数.

    Since it seems like EntityBase may be a base class for multiple derived classes, I wrote the converter to work for all derived types of EntityBase with the assumption that they all have a parameterized constructor with the same signature.

    请注意,我要从JSON中获取 EntityName .如果您希望将其硬编码为"Story" ,则可以这样做,但仍应将 EntityName 属性的实际名称添加到 usedParameters 集合,以防止创建具有相同名称的动态属性.

    Note I am taking the EntityName from the JSON. If you would prefer to hardcode it to "Story" you could do that, but you should still add the actual name of the EntityName property to the usedParameters collection to prevent a dynamic property with the same name from getting created.

    .net小提琴示例工作此处.

    Sample working .Net fiddle here.

    这篇关于带有DynamicObject和TypeCreationConverter的JsonConvert.DeserializeObject的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

  • 查看全文
    相关文章
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆