带接口的 JsonConverter [英] JsonConverter with Interface

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

问题描述

我有一个来自客户端并自动从 Web Api 2 反序列化的对象.

现在我的模型的一个属性有问题.此属性CurrentField"属于 IField 类型,此接口有 2 种不同的实现.

这是我的模型(只是一个假人)

公共类MyTest{公共 IField CurrentField {get;set;}}公共接口 IField{字符串名称{get;set;}}公共字段 1 : IField{公共字符串名称 {get;set;}公共 int MyValue {get;set;}}公共字段2:IField{公共字符串名称 {get;set;}公共字符串 MyStringValue {get;set;}}

我尝试创建一个自定义 JsonConverter 以找出我的客户端对象的类型(Field1 或 Field2),但我不知道如何.

我的转换器被调用,当我调用时我可以看到对象var obj = JObject.load(reader);

但是我怎样才能知道它是什么类型呢?我不能做这样的事情

if(obj is Field1) ...

这是我应该检查的方法吗?

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)

解决方案

如何在使用 Json.NET 反序列化接口时自动选择具体类型

解决问题的最简单方法是使用 TypeNameHandling = TypeNameHandling.Auto.如果这样做,您的 JSON 将包含为 IFIeld 属性序列化的实际类型,如下所示:

<块引用>

<代码>{当前字段":{"$type": "MyNamespace.Field2, MyAssembly","姓名": "姓名","MyStringValue": "我的字符串值"}}

但是,请注意 Newtonsoft 文档中的这一警告::><块引用>

当您的应用程序从外部源反序列化 JSON 时,应谨慎使用 TypeNameHandling.使用 None 以外的值反序列化时,应使用自定义 SerializationBinder 验证传入类型.

有关为什么需要这样做的讨论,请参阅 Newtonsoft Json 中的 TypeNameHandling 警告如何配置Json.NET 创建易受攻击的 Web API,以及 Alvaro Muñoz &Oleksandr Mirosh 的黑帽论文 https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf

如果由于某种原因您无法更改服务器输出的内容,您可以创建一个 JsonConverter 将 JSON 加载到 JObject 并检查实际存在哪些字段,然后搜索可能的具体类型以找到具有相同属性的类型:

公共类 JsonDerivedTypeConverer: JsonConverter{公共 JsonDerivedTypeConverer() { }公共 JsonDerivedTypeConverer(参数类型 [] 类型){this.DerivedTypes = 类型;}只读 HashSet派生类型 = 新 HashSet();公共 IEnumerable派生类型{得到{返回derivedTypes.ToArray();}放{如果(值 == 空)抛出新的 ArgumentNullException();派生类型.清除();如果(值!= null)派生类型.UnionWith(值);}}JsonObjectContract FindContract(JObject obj, JsonSerializer 序列化器){列表bestContracts = new List();foreach(派生类型中的 var 类型){如果(类型.IsAbstract)继续;var contract = serializer.ContractResolver.ResolveContract(type) as JsonObjectContract;如果(合同==空)继续;if (obj.Properties().Select(p => p.Name).Any(n => contract.Properties.GetClosestMatchProperty(n) == null))继续;if (bestContracts.Count == 0 || bestContracts[0].Properties.Count > contract.Properties.Count){bestContracts.Clear();bestContracts.Add(contract);}否则如果(contract.Properties.Count == bestContracts[0].Properties.Count){bestContracts.Add(contract);}}返回 bestContracts.Single();}public override bool CanConvert(Type objectType){返回 objectType == typeof(T);}公共覆盖对象 ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer){if (reader.TokenType == JsonToken.Null)返回空;var obj = JObject.Load(reader);//如果当前令牌不是对象,则抛出异常.var contract = FindContract(obj, serializer);如果(合同==空)throw new JsonSerializationException("没有找到" + obj.ToString());if (existingValue == null || !contract.UnderlyingType.IsAssignableFrom(existingValue.GetType()))existingValue = contract.DefaultCreator();使用 (var sr = obj.CreateReader()){serializer.Populate(sr, existingValue);}返回现有值;}公共覆盖 bool CanWrite { get { return false;} }public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer){抛出新的 NotImplementedException();}}

然后您可以将其作为转换器应用于 IField:

[JsonConverter(typeof(JsonDerivedTypeConverer), new object [] { new Type [] { typeof(Field1), typeof(Field2) } })]公共接口 IField{字符串名称 { 获取;放;}}

请注意,此解决方案有点脆弱.如果服务器省略了 MyStringValueMyValue 字段(因为它们具有默认值和 DefaultValueHandling = DefaultValueHandling.Ignore,例如)那么转换器将不知道要创建哪种类型并将抛出异常.类似地,如果实现 IField 的两个具体类型具有相同的属性名称,只是类型不同,转换器将抛出异常.使用 TypeNameHandling.Auto 可以避免这些潜在问题.

更新

以下版本会检查 "$type" 参数是否存在,如果 TypeNameHandling != TypeNameHandling.None,则使用默认序列化.它必须做一些技巧来防止回退时的无限递归:

公共类 JsonDerivedTypeConverer: JsonConverter{公共 JsonDerivedTypeConverer() { }公共 JsonDerivedTypeConverer(参数类型 [] 类型){this.DerivedTypes = 类型;}只读 HashSet派生类型 = 新 HashSet();公共 IEnumerable派生类型{得到{返回derivedTypes.ToArray();}放{派生类型.清除();如果(值!= null)派生类型.UnionWith(值);}}JsonObjectContract FindContract(JObject obj, JsonSerializer 序列化器){列表bestContracts = new List();foreach(派生类型中的 var 类型){如果(类型.IsAbstract)继续;var contract = serializer.ContractResolver.ResolveContract(type) as JsonObjectContract;如果(合同==空)继续;if (obj.Properties().Select(p => p.Name).Where(n => n != "$type").Any(n => contract.Properties.GetClosestMatchProperty(n) ==空值))继续;if (bestContracts.Count == 0 || bestContracts[0].Properties.Count > contract.Properties.Count){bestContracts.Clear();bestContracts.Add(contract);}否则如果(contract.Properties.Count == bestContracts[0].Properties.Count){bestContracts.Add(contract);}}返回 bestContracts.Single();}public override bool CanConvert(Type objectType){返回 objectType == typeof(T);}公共覆盖对象 ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer){if (reader.TokenType == JsonToken.Null)返回空;var obj = JObject.Load(reader);//如果当前标记不是对象,则抛出异常.if (obj["$type"] != null && serializer.TypeNameHandling != TypeNameHandling.None){//在列表中使用显式转换器时防止无限递归.varremoved = serializer.Converters.Remove(this);尝试{//在类型上使用 JsonConverterAttribute 时避免无限递归:反序列化为对象.返回 obj.ToObject(typeof(object), 序列化器);}最后{如果(已删除)serializer.Converters.Add(this);}}别的{var contract = FindContract(obj, serializer);如果(合同==空)throw new JsonSerializationException("没有找到" + obj.ToString());if (existingValue == null || !contract.UnderlyingType.IsAssignableFrom(existingValue.GetType()))existingValue = contract.DefaultCreator();使用 (var sr = obj.CreateReader()){serializer.Populate(sr, existingValue);}返回现有值;}}公共覆盖 bool CanWrite { get { return false;} }public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer){抛出新的 NotImplementedException();}}

I have an object which comes from the client and get deserialized from the Web Api 2 automatically.

Now I have a problem with one property of my model. This property "CurrentField" is of Type IField and there are 2 different Implementations of this interface.

This is my model (just a dummy)

public class MyTest
{
    public IField CurrentField {get;set;}
}

public interface IField{
    string Name {get;set;}
}

public Field1 : IField{
    public string Name {get;set;}
    public int MyValue {get;set;}
}

public Field2 : IField{
    public string Name {get;set;}
    public string MyStringValue {get;set;}
}

I tried to create a custom JsonConverter to find out of what type my object from the client is (Field1 or Field2) but I just don't know how.

My Converter gets called and I can see the object when I call var obj = JObject.load(reader);

but how can I find out what type it is? I can't do something like

if(obj is Field1) ...

this is the method where I should check this right?

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)

解决方案

How to automatically select a concrete type when deserializing an interface using Json.NET

The easiest way to solve your problem is to serialize and deserialize your JSON (on both the client and server sides) with TypeNameHandling = TypeNameHandling.Auto. If you do, your JSON will include the actual type serialized for an IFIeld property, like so:

{
  "CurrentField": {
    "$type": "MyNamespace.Field2, MyAssembly",
    "Name": "name",
    "MyStringValue": "my string value"
  }
}

However, note this caution from the Newtonsoft docs:

TypeNameHandling should be used with caution when your application deserializes JSON from an external source. Incoming types should be validated with a custom SerializationBinder when deserializing with a value other than None.

For a discussion of why this may be necessary, see TypeNameHandling caution in Newtonsoft Json, How to configure Json.NET to create a vulnerable web API, and Alvaro Muñoz & Oleksandr Mirosh's blackhat paper https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf

If for whatever reason you cannot change what the server outputs, you can create a JsonConverter that loads the JSON into a JObject and checks to see what fields are actually present, then searches through possible concrete types to find one with the same properties:

public class JsonDerivedTypeConverer<T> : JsonConverter
{
    public JsonDerivedTypeConverer() { }

    public JsonDerivedTypeConverer(params Type[] types)
    {
        this.DerivedTypes = types;
    }

    readonly HashSet<Type> derivedTypes = new HashSet<Type>();

    public IEnumerable<Type> DerivedTypes
    {
        get
        {
            return derivedTypes.ToArray(); 
        }
        set
        {
            if (value == null)
                throw new ArgumentNullException();
            derivedTypes.Clear();
            if (value != null)
                derivedTypes.UnionWith(value);
        }
    }

    JsonObjectContract FindContract(JObject obj, JsonSerializer serializer)
    {
        List<JsonObjectContract> bestContracts = new List<JsonObjectContract>();
        foreach (var type in derivedTypes)
        {
            if (type.IsAbstract)
                continue;
            var contract = serializer.ContractResolver.ResolveContract(type) as JsonObjectContract;
            if (contract == null)
                continue;
            if (obj.Properties().Select(p => p.Name).Any(n => contract.Properties.GetClosestMatchProperty(n) == null))
                continue;
            if (bestContracts.Count == 0 || bestContracts[0].Properties.Count > contract.Properties.Count)
            {
                bestContracts.Clear();
                bestContracts.Add(contract);
            }
            else if (contract.Properties.Count == bestContracts[0].Properties.Count)
            {
                bestContracts.Add(contract);
            }
        }
        return bestContracts.Single();
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(T);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var obj = JObject.Load(reader); // Throws an exception if the current token is not an object.
        var contract = FindContract(obj, serializer);
        if (contract == null)
            throw new JsonSerializationException("no contract found for " + obj.ToString());
        if (existingValue == null || !contract.UnderlyingType.IsAssignableFrom(existingValue.GetType()))
            existingValue = contract.DefaultCreator();
        using (var sr = obj.CreateReader())
        {
            serializer.Populate(sr, existingValue);
        }
        return existingValue;
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Then you can apply that as a converter to IField:

[JsonConverter(typeof(JsonDerivedTypeConverer<IField>), new object [] { new Type [] { typeof(Field1), typeof(Field2) } })]
public interface IField
{
    string Name { get; set; }
}

Note that this solution is a little fragile. If the server omits the MyStringValue or MyValue fields (because they have default value and DefaultValueHandling = DefaultValueHandling.Ignore, for example) then the converter won't know which type to create and will throw an exception. Similarly, if two concrete types implementing IField have the same property names, differing only in type, the converter will throw an exception. Using TypeNameHandling.Auto avoids these potential problems.

Update

The following version checks to see if the "$type" parameter is present, and if TypeNameHandling != TypeNameHandling.None, falls back on default serialization. It has to do a couple of tricks to prevent infinite recursion when falling back:

public class JsonDerivedTypeConverer<T> : JsonConverter
{
    public JsonDerivedTypeConverer() { }

    public JsonDerivedTypeConverer(params Type[] types)
    {
        this.DerivedTypes = types;
    }

    readonly HashSet<Type> derivedTypes = new HashSet<Type>();

    public IEnumerable<Type> DerivedTypes
    {
        get
        {
            return derivedTypes.ToArray(); 
        }
        set
        {
            derivedTypes.Clear();
            if (value != null)
                derivedTypes.UnionWith(value);
        }
    }

    JsonObjectContract FindContract(JObject obj, JsonSerializer serializer)
    {
        List<JsonObjectContract> bestContracts = new List<JsonObjectContract>();
        foreach (var type in derivedTypes)
        {
            if (type.IsAbstract)
                continue;
            var contract = serializer.ContractResolver.ResolveContract(type) as JsonObjectContract;
            if (contract == null)
                continue;
            if (obj.Properties().Select(p => p.Name).Where(n => n != "$type").Any(n => contract.Properties.GetClosestMatchProperty(n) == null))
                continue;
            if (bestContracts.Count == 0 || bestContracts[0].Properties.Count > contract.Properties.Count)
            {
                bestContracts.Clear();
                bestContracts.Add(contract);
            }
            else if (contract.Properties.Count == bestContracts[0].Properties.Count)
            {
                bestContracts.Add(contract);
            }
        }
        return bestContracts.Single();
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(T);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var obj = JObject.Load(reader); // Throws an exception if the current token is not an object.
        if (obj["$type"] != null && serializer.TypeNameHandling != TypeNameHandling.None)
        {
            // Prevent infinite recursion when using an explicit converter in the list.
            var removed = serializer.Converters.Remove(this);
            try
            {
                // Kludge to prevent infinite recursion when using JsonConverterAttribute on the type: deserialize to object.
                return obj.ToObject(typeof(object), serializer);
            }
            finally
            {
                if (removed)
                    serializer.Converters.Add(this);
            }
        }
        else
        {
            var contract = FindContract(obj, serializer);
            if (contract == null)
                throw new JsonSerializationException("no contract found for " + obj.ToString());
            if (existingValue == null || !contract.UnderlyingType.IsAssignableFrom(existingValue.GetType()))
                existingValue = contract.DefaultCreator();
            using (var sr = obj.CreateReader())
            {
                serializer.Populate(sr, existingValue);
            }
            return existingValue;
        }
    }

    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

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

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