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.
似乎 DeserializeObject
在 CustomCreationConverter
创建的对象上调用了 PopulateObject
。当我尝试手动执行此操作时,错误保持不变
JsonConvert.PopulateObject(JSON, new EntityBase("Story"));
我进一步假设 PopulateObject
不检查目标类型是否源自 DynamicObject
,因此不会退回到 TrySetMember
。
请注意,我对 EntityBase
类型定义没有影响,它来自外部库,无法更改。
任何见解将不胜感激!
编辑:添加示例:https://dotnetfiddle.net/EGOCFU
您似乎无意中发现了 Json.NET 对反序列化动态对象(定义为生成 JsonDynamicContract
的对象)的支持中的一些错误或限制:
不支持参数化构造函数。即使标有 [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.
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.
我有一个 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.
似乎 DeserializeObject
在 CustomCreationConverter
创建的对象上调用了 PopulateObject
。当我尝试手动执行此操作时,错误保持不变
JsonConvert.PopulateObject(JSON, new EntityBase("Story"));
我进一步假设 PopulateObject
不检查目标类型是否源自 DynamicObject
,因此不会退回到 TrySetMember
。
请注意,我对 EntityBase
类型定义没有影响,它来自外部库,无法更改。
任何见解将不胜感激!
编辑:添加示例:https://dotnetfiddle.net/EGOCFU
您似乎无意中发现了 Json.NET 对反序列化动态对象(定义为生成 JsonDynamicContract
的对象)的支持中的一些错误或限制:
不支持参数化构造函数。即使标有
[JsonConstructor]
也不会被使用。此处预加载所有属性的必要逻辑似乎在
JsonSerializerInternalReader.CreateDynamic()
. Compare withJsonSerializerInternalReader.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.
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.