c# - 带接口(interface)的 JsonConverter

标签 c# json asp.net-web-api2

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

现在我的模型的一个属性有问题。此属性“CurrentField”属于 IField 类型,此接口(interface)有 2 种不同的实现。

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

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;}
}

我试图创建一个自定义 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 反序列化接口(interface)时如何自动选择具体类型

解决您的问题的最简单方法是使用 TypeNameHandling = TypeNameHandling.Auto 序列化和反序列化您的 JSON(在客户端和服务器端)。 .如果这样做,您的 JSON 将包含为 IFIeld 序列化的实际类型。属性(property),像这样:

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


但是,请注意 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.



有关为什么这可能是必要的讨论,请参阅 TypeNameHandling caution in Newtonsoft Json , How to configure Json.NET to create a vulnerable 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并检查实际存在哪些字段,然后搜索可能的具体类型以找到具有相同属性的类型:
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();
    }
}

然后你可以将它作为转换器应用到 IField :
[JsonConverter(typeof(JsonDerivedTypeConverer<IField>), new object [] { new Type [] { typeof(Field1), typeof(Field2) } })]
public interface IField
{
    string Name { get; set; }
}

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

更新

以下版本检查 "$type"参数存在,如果 TypeNameHandling != TypeNameHandling.None , 回退到默认序列化。它必须做一些技巧来防止回退时的无限递归:
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();
    }
}

关于c# - 带接口(interface)的 JsonConverter,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33321698/

相关文章:

c# - 使用 MySQL 后端开发 C# 项目的最佳方法是什么?

android - 使用 mySQL 数据库中的数据填充 ListView

javascript - d3.js 结合分层边缘捆绑和径向 Reingold–Tilford 树 + 数据

c# - 使用 Windows 客户端应用程序防止跨站点请求伪造攻击

javascript - 将动态输入表单数据保存到数据库kendo ui

security - 验证 WebAPI 中的 JWT 声明

c# - 由于使用 sitecore 的对象的当前状态,该操作无效

c# - Entity Framework 和 SQL Server Profiler

c# - 如果文件存在,覆盖它?

java - 为什么 Jersey 客户端在我的列表应该为空时将对象插入到列表中?