带接口的 JsonConverter

分享于2023年03月15日 asp.net-web-api2 c# json 问答
【问题标题】:JsonConverter with Interface带接口的 JsonConverter
【发布时间】:2016-01-24 03:11:08
【问题描述】:

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

现在我的模型的一个属性有问题。此属性“CurrentField”属于 IField 类型,该接口有 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)

  • This SO this 应该可以帮到你。
  • 为什么不直接使用 TypeNameHandling = TypeNameHandling.Auto ?它正是针对这种情况而设计的,并在 JSON 中记录了用于接口的实际 .Net 类型。
  • @dbc:感谢您的快速回答,但即使我将其添加到我的配置中,它仍然无法正常工作

【解决方案1】:

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

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

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

但是,请注意来自 Newtonsoft docs 的警告:

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

关于为什么这可能是必要的讨论,请参阅 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 : JsonConverter
{
    public JsonDerivedTypeConverer() { }

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

    readonly HashSet derivedTypes = new HashSet();

    public IEnumerable 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 bestContracts = new List();
        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), new object [] { new Type [] { typeof(Field1), typeof(Field2) } })]
public interface IField
{
    string Name { get; set; }
}

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

更新

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

public class JsonDerivedTypeConverer : JsonConverter
{
    public JsonDerivedTypeConverer() { }

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

    readonly HashSet derivedTypes = new HashSet();

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

    JsonObjectContract FindContract(JObject obj, JsonSerializer serializer)
    {
        List bestContracts = new List();
        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();
    }
}

【讨论】:

  • 天哪,非常感谢!!我将您的代码复制到 VS,它开箱即用(我只需要用我的真实模型类替换虚拟类)。但是为什么实现这种行为如此复杂呢?为什么我必须创建这样一个(不错的)JsonConverter 来转换接口?
  • 我无法编辑我的评论...首先我要感谢您快速且几乎完美的回答。当客户端数据不包含 $type-property 时,它可以完美运行。但是当它包含 $type-property 这个 bestContracts.Single();抛出异常,因为它没有元素。对于循环中的每种类型,它在这种情况下运行 if (obj.Properties().Select(p => p.Name).Any(n => contract.Properties.GetClosestMatchProperty(n) == null)) continue;你知道如何告诉他 $type-property 应该被忽略吗?
  • @TobiasKoller - 这有点复杂,因为我写的很笼统。如果硬编码,它看起来会更简单。如果存在 $type 属性,那么您不需要任何转换器,Json.NET 将创建正确的类型——假设您在客户端和服务器设置中都设置了 TypeNameHandling = TypeNameHandling.Auto
  • @TobiasKoller - 进入转换器后回到默认行为并不容易,转换器只是再次递归调用,导致堆栈溢出,因此需要一些 kludge。这就是为什么我建议不要同时实施这两种解决方案。
  • @TobiasKoller - 好的,我对其进行了增强以检查 $type 。如您所见,它更复杂。