在 Asp.Net Web API 中将 JSON 反序列化为派生类型

Posted

技术标签:

【中文标题】在 Asp.Net Web API 中将 JSON 反序列化为派生类型【英文标题】:Deserialising JSON to derived types in Asp.Net Web API 【发布时间】:2012-09-20 06:19:47 【问题描述】:

我正在调用我的 WebAPI 的一个方法,发送一个我想与模型匹配(或绑定)的 JSON。

在控制器中我有一个类似的方法:

public Result Post([ModelBinder(typeof(CustomModelBinder))]MyClass model);

作为参数给出的'MyClass'是一个抽象类。我希望根据传递的 json 类型,实例化正确的继承类。

为了实现它,我正在尝试实现一个自定义活页夹。问题是(我不知道它是否非常基本,但我找不到任何东西)我不知道如何检索请求中的原始 JSON(或者更好的是某种序列化)。

我明白了:

actionContext.Request.Content

但所有方法都公开为异步。我不知道这适合将生成模型传递给控制器​​方法...

【问题讨论】:

【参考方案1】:

您不需要自定义模型绑定器。您也不需要处理请求管道。

看看另一个 SO:How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects?。

我以此作为我自己解决同一问题的基础。

从该 SO 中引用的 JsonCreationConverter<T> 开始(稍作修改以解决响应中类型序列化的问题):

public abstract class JsonCreationConverter<T> : JsonConverter

    /// <summary>
    /// this is very important, otherwise serialization breaks!
    /// </summary>
    public override bool CanWrite
    
        get
        
            return false;
        
    
    /// <summary> 
    /// Create an instance of objectType, based properties in the JSON object 
    /// </summary> 
    /// <param name="objectType">type of object expected</param> 
    /// <param name="jObject">contents of JSON object that will be 
    /// deserialized</param> 
    /// <returns></returns> 
    protected abstract T Create(Type objectType, JObject jObject);

    public override bool CanConvert(Type objectType)
    
        return typeof(T).IsAssignableFrom(objectType);
    

    public override object ReadJson(JsonReader reader, Type objectType,
      object existingValue, JsonSerializer serializer)
    
        if (reader.TokenType == JsonToken.Null)
            return null;
        // Load JObject from stream 
        JObject jObject = JObject.Load(reader);

        // Create target object based on JObject 
        T target = Create(objectType, jObject);

        // Populate the object properties 
        serializer.Populate(jObject.CreateReader(), target);

        return target;
    

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

现在您可以使用 JsonConverterAttribute 注释您的类型,将 Json.Net 指向自定义转换器:

[JsonConverter(typeof(MyCustomConverter))]
public abstract class BaseClass
  private class MyCustomConverter : JsonCreationConverter<BaseClass>
  
     protected override BaseClass Create(Type objectType, 
       Newtonsoft.Json.Linq.JObject jObject)
     
       //TODO: read the raw JSON object through jObject to identify the type
       //e.g. here I'm reading a 'typename' property:

       if("DerivedType".Equals(jObject.Value<string>("typename")))
       
         return new DerivedClass();
       
       return new DefaultClass();

       //now the base class' code will populate the returned object.
     
  


public class DerivedClass : BaseClass 
  public string DerivedProperty  get; set; 


public class DefaultClass : BaseClass 
  public string DefaultProperty  get; set; 

现在您可以使用基本类型作为参数:

public Result Post(BaseClass arg) 


如果我们要发帖:

 typename: 'DerivedType', DerivedProperty: 'hello' 

那么arg 将是DerivedClass 的一个实例,但如果我们发布:

 DefaultProperty: 'world' 

然后你会得到一个DefaultClass 的实例。

编辑 - 为什么我更喜欢这种方法而不是 TypeNameHandling.Auto/All

我确实相信使用 JotaBe 支持的 TypeNameHandling.Auto/All 并不总是理想的解决方案。在这种情况下很可能是这样 - 但我个人不会这样做,除非:

我的 API只有会被我或我的团队使用 我不在乎拥有双 XML 兼容端点

当使用 Json.Net TypeNameHandling.AutoAll 时,您的 Web 服务器将开始以 MyNamespace.MyType, MyAssemblyName 格式发送类型名称。

我在 cmets 中说过,我认为这是一个安全问题。在我从 Microsoft 阅读的一些文档中提到了这一点。它似乎不再被提及,但我仍然觉得这是一个有效的担忧。我永远不想向外界公开命名空间限定的类型名称和程序集名称。它增加了我的攻击面。所以,是的,我的 API 类型不能有Object 属性/参数,但谁能说我的网站的其余部分完全没有漏洞?谁说未来的端点不会暴露利用类型名称的能力?为什么要抓住这个机会,因为它更容易?

另外 - 如果您正在编写一个“适当的”API,即专门供第三方使用,而不仅仅是供您自己使用,并且您正在使用 Web API,那么您很可能希望利用 JSON/XML内容类型处理(至少)。看看您在尝试编写易于使用的文档方面取得了多大的成就,这些文档针对 XML 和 JSON 格式以不同的方式引用您的所有 API 类型。

通过重写 JSON.Net 对类型名称的理解方式,您可以使两者保持一致,为您的调用者在 XML/JSON 之间进行选择完全基于口味,而不是因为类型名称更容易记住一个或另一个。

【讨论】:

已在您的帖子中添加了一条评论,说明为什么您的解决方案虽然正确,但不应真正“在野外”使用。 为了表明我不只是一个混蛋——我已经 +1 了你的答案,因为它在一定程度上是有效的——并且很可能是最简单的方法问题。我只是不认为它在所有情况下都应该被视为灵丹妙药。 我已经更新了我的答案,以反映您的解决方案何时是最佳选择。我认为这是一次非常有建设性的讨论。您也有我的 +1,因为您的解决方案是对 JSON 反序列化过程进行各种自定义的完美起点。而现在,这两个答案都得到了改善。 只是为了添加注释。过去我使用了 $type 解决方案,但是对于我现在正在使用 AngularJS 进行的项目,它在 json 中用 $ 剥离任何东西时存在问题。此外,使用 TypeScript 类,我无法弄清楚如何确保 $type 包含在 JSON 序列化中。我确信这些问题有解决方案,但这种方法让我能够轻松克服这些问题。 我对 $type 最大的抱怨是我不想在重构服务器端代码时破坏 API 兼容性,例如重命名类型或将其移动到另一个命名空间或程序集。例如,存储在客户端存储中并在以后使用的 DTO 将突然不再工作。如果改为使用枚举值或字符串,则可以完全控制 API 的向后兼容性。【参考方案2】:

您不需要自己实现它。 JSON.NET 对它有原生支持。

您必须为 JSON 格式化程序指定 desired TypeNameHandling option,如下所示(在 global.asax 应用程序启动事件中):

JsonSerializerSettings serializerSettings = GlobalConfiguration.Configuration
   .Formatters.JsonFormatter.SerializerSettings;
serializerSettings.TypeNameHandling = TypeNameHandling.Auto;

如果您指定Auto,就像上面的示例一样,参数将被反序列化为对象的$type 属性中指定的类型。如果$type 属性缺失,它将被反序列化为参数的类型。因此,您只需在传递派生类型的参数时指定类型。 (这是最灵活的选项)。

例如,如果您将此参数传递给 Web API 操作:

var param = 
    $type: 'MyNamespace.MyType, MyAssemblyName', // .NET fully qualified name
    ... // object properties
;

参数将被反序列化为MyNamespace.MyType类的对象。

这也适用于子属性,即你可以有一个像这样的对象,它指定一个内部属性是给定类型的

var param =  
   myTypedProperty: 
      $type: `...`
      ...
;

在这里你可以看到sample on JSON.NET documentation of TypeNameHandling.Auto。

This works at least since JSON.NET 4 release.

注意

你不需要用属性来装饰任何东西,或者做任何其他的定制。它无需更改您的 Web API 代码即可工作。

重要提示

The $type must be the first property of the JSON serialized object。如果不是,它将被忽略。

与自定义 JsonConverter/JsonConverterAttribute 的比较

我在比较原生解决方案to this answer。

实现JsonConverter/JsonConverterAttribute

您需要实现自定义JsonConverter,以及自定义JsonConverterAttribute 你需要使用属性来标记参数 您需要事先了解参数预期的可能类型 只要您的类型或属性发生变化,您就需要实现或更改JsonConverter 的实现 有magic strings 的代码味道,表示预期的属性名称 您没有实现可用于任何类型的通用内容 你正在重新发明***

在答案的作者中有一条关于安全的评论。除非你做错了什么(比如为你的参数接受一个过于通用的类型,比如Object),否则不会有获得错误类型实例的风险:JSON.NET 本机解决方案只实例化参数类型的对象,或者派生自它的类型(如果不是,你会得到null)。

而这些是 JSON.NET 原生解决方案的优势:

你不需要实现任何东西(你只需要在你的应用中配置一次TypeNameHandling) 您无需在操作参数中使用属性 您不需要事先知道可能的参数类型:您只需要知道基本类型,并在参数中指定它(可以是抽象类型,使多态性更加明显) 该解决方案适用于大多数情况(1),无需更改任何内容 此解决方案经过广泛测试和优化 你不需要魔术字符串 实现是通用的,可以接受任何派生类型

(1):如果你想接收不继承自相同基类型的参数值,这将不起作用,但我认为这样做没有意义

所以我找不到任何缺点,并在 JSON.NET 解决方案上找到了许多优点。

为什么要使用自定义 JsonConverter/JsonConverterAttribute

这是一个很好的工作解决方案,允许自定义,可以修改或扩展以适应您的特定情况。

如果你想做一些原生解决方案无法做到的事情,比如自定义类型名称,或者根据可用的属性名称推断参数的类型,那么请使用适合你自己情况的解决方案。另一个无法自定义,无法满足您的需求。

【讨论】:

是的,你是对的 - 但这是不适合我的情况(包括我的情况)的原因。使用 Json.Net 的内置类型名称处理功能存在 安全问题,因为它有效地允许恶意调用者绑定您的任何类型 - 或任何 .Net 类型。该文档对此进行了具体说明。我的解决方案提供了一个抽象,您可以精确控制可以绑定的类型。 JotaBe - 在没有首先考虑到这是一个适当缩减的答案的情况下,要非常小心地小跑“不通用”、“没有经过良好测试”和“可能性能较差”的短语,以免提供一堵代码墙,但很容易测试和扩展。其次,我基于此的解决方案是在非常繁忙的 Web API 环境中实现的更广泛的解决方案,它表现出色。是的 - $type 的东西在那里,并且有效。将安全问题视为“不是用例”是幼稚的。你也应该带着SerializationBinder 来回答我的观点。 Web API 文档中很早就提到了对安全性的担忧——而且,是的,谷歌似乎已经放弃了这一点。公平的做法;但我不是骗子,我确实读过这些担忧。即使没有其他人支持 - 我个人不想在我的 JSON 中编码 MyNamespace.MyType, MyAssembly 类型名称 - 我不想将我的真实类型名称任何暴露给外界;当一个简单的TypeName 就足够时,通过 JSON 使用我的 API 的第三方也不想使用 .Net 类型名称。不过,每个人都有自己的特点。 我看到的唯一安全问题是使用通用参数类型,如对象或动态,这是唯一允许实例化任意对象的情况。我只是想知道我是否还缺少其他东西。嘿!,我不是想看不起你的解决方案,或者认为你在撒谎,一点也不:我想表达一个公平的意见。我真的认为 JSON.NET 解决方案更通用,开箱即用(只需要一个参数基类),经过广泛的现场测试(在 Nuget 中有近 6M 的下载量),这比任何经过深思熟虑的单元测试套件都可以进行更多测试实施 好吧,那么您应该公平地说,TypeNameHandling 工作正常,这是一个很好的解决方案除非存在无法处理的问题。在这种情况下,您提供了一个经过测试的解决方案,该解决方案展示了如何进行反序列化的自定义实现,这对于需要特殊自定义的许多其他人很有用(例如,在您的情况下,您不想使用 .NET 完全限定名称) .我同意这一点!!对于这些情况,这是一个很好的解决方案(而且一点也不明显)。但是原生解决方案对大多数人来说仍然很好。【参考方案3】:

您可以正常调用异步方法,您的执行将被暂停,直到方法返回,您可以以标准方式返回模型。只需像这样拨打电话:

string jsonContent = await actionContext.Request.Content.ReadAsStringAsync();

它将为您提供原始 JSON。

【讨论】:

除非您对可能出现的死锁感到满意,否则不要使用“.Result”nitoprograms.blogspot.ch/2012/07/dont-block-on-async-code.html【参考方案4】:

如果您想使用 TypeNameHandling.Auto 但担心安全性或不喜欢需要该级别幕后知识的 api 使用者,您可以处理 $type 反序列化您自己。

public class InheritanceSerializationBinder : DefaultSerializationBinder

    public override Type BindToType(string assemblyName, string typeName)
    
        switch (typeName)
        
            case "parent[]": return typeof(Class1[]);
            case "parent": return typeof(Class1);
            case "child[]": return typeof(Class2[]);
            case "child": return typeof(Class2);
            default: return base.BindToType(assemblyName, typeName);
        
    

然后将其连接到 global.asax.Application__Start

var config = GlobalConfiguration.Configuration;
        config.Formatters.JsonFormatter.SerializerSettings = new JsonSerializerSettings  Binder = new InheritanceSerializationBinder() ;

最后,我在包含不同类型对象的属性上使用了包装类和 [JsonProperty(TypeNameHandling = TypeNameHandling.Auto)],因为我无法通过配置实际类来使其工作。

这种方法允许消费者在他们的请求中包含所需的信息,同时允许允许值的文档独立于平台、易于更改且易于理解。所有这些都无需编写您自己的转换器。

归功于:https://mallibone.com/post/serialize-object-inheritance-with-json.net 向我展示了该字段属性的自定义反序列化器。

【讨论】:

以上是关于在 Asp.Net Web API 中将 JSON 反序列化为派生类型的主要内容,如果未能解决你的问题,请参考以下文章

如何在 ASP.NET Web API 实现中将数组传递给 OData 函数?

Asp.Net Web API 2第十三课——ASP.NET Web API中的JSON和XML序列化

如何在 ASP.NET Web API 中为 Json.NET 设置自定义 JsonSerializerSettings?

使用 ASP.NET Web API 返回 JSON 文件

如何在 asp.net web api 中返回 json 错误消息?

如何在 Asp.net Core Web Api 中默认使用 Newtonsoft.Json?