JsonConvert.DeserializeObject 带有 DynamicObject 和 TypeCreationConverter

JsonConvert.DeserializeObject w/ DynamicObject and TypeCreationConverter

我有一个 class 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);

但是由于我没有空的默认构造函数并且无法更改 class 我选择了 CustomCreationConverter :

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

但是

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

投掷

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

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

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

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

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

任何见解将不胜感激!

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

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

  1. 不支持参数化构造函数。即使标有 [JsonConstructor] 也不会被使用。

    此处预加载所有属性的必要逻辑似乎在 JsonSerializerInternalReader.CreateDynamic(). Compare with JsonSerializerInternalReader.CreateNewObject() 中完全缺失,这表明需要什么。

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

    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.

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

    我不明白为什么这不能通过相当简单的代码重组来实现。您可能 submit an issue 要求这样做。如果实现了这一点,您的 StoryCreator 将按原样工作。

在没有#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 
    // 
    // by jbtule, https://whosebug.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 可能是多个派生 class 的基础 class,我编写了转换器以适用于 EntityBase 的所有派生类型假设它们都有一个具有相同签名的参数化构造函数。

请注意,我正在从 JSON 中提取 EntityName。如果您更愿意将其硬编码为 "Story" 您可以这样做,但您仍应将 EntityName 属性 的实际名称添加到 usedParameters 集合中以防止动态属性 与创建时同名。

示例工作.Net fiddle here.