【问题标题】:How do I get WebAPI to validate my JSON with JsonProperty(Required = Required.Always)?如何让 WebAPI 使用 JsonProperty(Required = Required.Always) 验证我的 JSON?
【发布时间】:2020-03-03 15:51:04
【问题描述】:
public class MyModel
{
    [JsonProperty(PropertyName = "foo", Required = Required.Always)]
    public String Bar;
}

public class MyController : ApiController
{
    public String PostVersion1([FromBody] MyModel myModel)
    {
        if (ModelState.IsValid)
        {
            if (myModel.Bar == null)
                return "What?!";
            else
                return "Input is valid.";
        }
        else
        {
            return "Input is invalid.";
        }
    }
}

结果:

Input              |Output
-------------------|------
{ "bad" : "test" } | What?!
{ "Bar" : "test" } | What?!
{ "foo" : "test" } | Input is valid.

显然支持 JsonPropertyAttribute,因为我可以设置 PropertyName 并使其生效。但是,我希望 ModelState.IsValid 对于前两个示例输入为 false,因为 Required JsonProprty 参数设置为 Always。

如果我只是通过 JsonConvert 运行它:

JsonConvert.DeserializeObject<MyModel>(@"{'bad':'test'}");

反序列化过程中按预期抛出异常:

Result Message: Newtonsoft.Json.JsonSerializationException : Required property 'foo' not found in JSON. Path '', line 1, position 14.

【问题讨论】:

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


    【解决方案1】:

    默认JsonMediaTypeFormatter确实不依赖JsonProperty来决定是否需要模型字段。但是它确实依赖于RequiredAttribute

    如果你想这样做,那么实现一个新的IRequiredMemberSelector 并将其设置为MediaTypeFormatter.RequiredMemberSelector。

    在您的IRequiredMemberSelector 实现中,您将获得MemberInfo。您可以使用它来评估模型成员是否具有 JsonProperty 属性以及是否设置了 required 标志,最后返回 true 或 false。此将传播到ModelState.IsValid 属性(虽然它不会使用 JSON.NET 错误消息,但会使用 DataAnnotations/WebApi 之一。

    如果你这样做,那么我建议你也保留默认行为。

    【讨论】:

    • 我不能使用RequiredAttribute,因为它似乎在JSON反序列化器的名称路由之前生效。在上面的示例中,{ "Bar" : "test" } 将验证,即使一旦它反序列化 Bar 将为 null。
    • 似乎RequiredMemberSelector 是先运行,然后在验证后反序列化类型。是否可以反序列化然后验证或一步完成,而不必用 JsonConvert.Deserialize 调用乱扔我的控制器操作?似乎这里更正确的操作过程(特别是因为我想在反序列化失败时返回错误)将首先反序列化,然后如果反序列化成功则调用该操作。如果我理解正确,则首先进行验证,然后再进行反序列化。
    • @MicahCaldwell:让我检查一下。
    • @MicahCaldwell:实际上,如果您先反序列化然后验证您会丢失 JSON,因此会丢失任何有意义的错误。您将无法像JsonConvert 那样输出错误。因此,它应该是相反的方式。还请查看JsonContractResolver.ConfigureProperty。它总是设置property.Required = Required.AllowNull,因此也需要更改。这发生在反序列化之前。
    • @MicahCaldwell:现在不确定它是否可以使用这条路径。如果您查看BaseJsonMediaTypeFormatter.ReadFromStream,很明显任何 JSON.NET 异常都会被静音。
    【解决方案2】:

    为了解决这个问题,我最终创建了自己的自定义 JSON.NET MediaTypeFormatter。我的格式化程序允许 JSON.NET 反序列化异常冒泡,导致异常信息返回给调用者。

    这是我构建的 MediaTypeFormatter:

    public class JsonMediaFormatter : MediaTypeFormatter
    {
        private readonly JsonSerializer _jsonSerializer = new JsonSerializer();
    
        public JsonMediaFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
        }
    
        public override Boolean CanReadType(Type type)
        {
            if (type == null)
                return false;
    
            return true;
        }
    
        public override Boolean CanWriteType(Type type)
        {
            if (type == null)
                return false;
    
            return true;
        }
    
        public override Task<Object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
        {
            return Task.FromResult(Deserialize(readStream, type));
        }
    
        public override Task WriteToStreamAsync(Type type, Object value, Stream writeStream, HttpContent content, TransportContext transportContext, CancellationToken cancellationToken)
        {
            Serialize(writeStream, value);
            return Task.FromResult(0);
        }
    
        private Object Deserialize(Stream readStream, Type type)
        {
            var streamReader = new StreamReader(readStream);
            return _jsonSerializer.Deserialize(streamReader, type);
        }
    
        private void Serialize(Stream writeStream, Object value)
        {
            var streamWriter = new StreamWriter(writeStream);
            _jsonSerializer.Serialize(streamWriter, value);
            streamWriter.Flush();
        }
    }
    

    为了在内置的格式化程序上使用这个格式化程序,我在我的 WebApiConfig 中添加了这一行:

    config.Formatters.Insert(0, new Formatters.JsonMediaFormatter());
    

    通过在索引 0 处插入它,它优先于内置格式化程序。如果您愿意,可以删除内置的 JSON 格式化程序。

    在这种情况下,ModelState 在操作中始终有效,因为如果反序列化失败,则会在触发操作之前向用户抛出异常。需要做更多的工作才能使用空的FromBody 参数执行操作。

    【讨论】:

    • 在发布版本中,异常似乎被吞没了,一个通用异常被发回给用户。有必要进行研究以使其发回有用的错误。
    • 解决了使用异常过滤器发回的错误,该过滤器在看到 JsonSerializationException 时会构建特殊响应 (400)。
    • 你为什么使用Task.Factory.StartNew()??只需使用 Task.FromResult(...)。将 Task.Factory.StarNew() 用于 CPU 绑定操作是一种不好的做法。
    • 已修复。改为任务。来自结果。
    • Task.FromResult(Serialize(...)) 无法编译,因为它返回 void。您应该先拨打Serialize(),然后拨打Task.FromResult(0)。
    【解决方案3】:

    我知道这是一个老问题,但我是这样解决的:

    var formatter = new JsonMediaTypeFormatter {
        SerializerSettings = {
            ContractResolver = new DefaultContractResolver(true)
        }
    };
    configuration.Formatters.Insert(0, formatter);
    

    解析错误随后将包含在ModelState中

    【讨论】:

      【解决方案4】:

      如果你只想支持 JSON,那么你可以这样做:

      public String PostVersion1([FromBody] JObject json)
      {
        if(json == null) {
          // Invalid JSON or wrong Content-Type
          throw new HttpResponseException(HttpStatusCode.BadRequest);
        }
      
        MyModel model;
        try
        {
          model = json.ToObject<MyModel>();
        }
        catch(JsonSerializationException e)
        {
          // Serialization failed
          throw new HttpResponseException(HttpStatusCode.BadRequest);
        }
      }
      

      但您不想在每个请求处理程序中都这样做。这是[FromBody] 属性的多个缺陷之一。更多详情请看这里:https://stackoverflow.com/a/52877955/2279059

      【讨论】:

        猜你喜欢
        • 2018-07-17
        • 1970-01-01
        • 1970-01-01
        • 2023-03-25
        • 1970-01-01
        • 2016-10-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多