【问题标题】:Web API/JsonMediaTypeFormatter accepts Invalid JSON and passes null argument to actionWeb API/JsonMediaTypeFormatter 接受 Invalid JSON 并将 null 参数传递给操作
【发布时间】:2013-02-28 18:18:11
【问题描述】:

我有以下型号:

public class Resource
{
    [DataMember(IsRequired = true)]
    [Required]
    public bool IsPublic { get; set; }

    [DataMember(IsRequired = true)]
    [Required]
    public ResourceKey ResourceKey { get; set; }
}

public class ResourceKey
{
    [StringLength(50, MinimumLength = 1)]
    [Required]
    public string SystemId { get; set; }

    [StringLength(50, MinimumLength = 1)]
    [Required]
    public string SystemDataIdType { get; set; }

    [StringLength(50, MinimumLength = 1)]
    [Required]
    public string SystemEntityType { get; set; }

    [StringLength(50, MinimumLength = 1)]
    [Required]
    public string SystemDataId { get; set; }
}

我有以下动作方法签名:

public HttpResponseMessage PostResource(Resource resource)

我在正文中使用 JSON 发送以下请求(属性“IsPublic”的故意无效值):

Request Method:POST
Host: localhost:63307
Connection: keep-alive
Content-Length: 477
User-Agent: Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.97 Safari/537.22
Origin: chrome-extension://hgmloofddffdnphfgcellkdfbfbjeloo
Content-Type: application/json
Accept: */*
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

{
    "IsPublic": invalidvalue,   
    "ResourceKey":{     
        "SystemId": "asdf",
        "SystemDataIdType": "int",
        "SystemDataId": "Lorem ipsum",
        "SystemEntityType":"EntityType"
    },    
}

这是无效的 JSON - 通过 JSONLint 运行它,它会告诉你:

第 2 行解析错误:

{ "IsPublic": 无效值,

..................^ 期望 'STRING'、'NUMBER'、'NULL'、'TRUE'、'FALSE'、'{'、'['

ModelState.IsValid 属性为 'true' - 为什么???

此外,格式化程序似乎放弃了反序列化并简单地将“资源”参数作为 null 传递给操作方法,而不是引发验证错误!

请注意,如果我为其他属性输入无效值也会发生这种情况,例如替换:

"SystemId": notAnObjectOrLiteralOrArray

但是,如果我发送以下 JSON 并为“SystemId”属性发送一个特殊的 undefined 值:

{
    "IsPublic": true,   
    ResourceKey:{       
        "SystemId": undefined,
        "SystemDataIdType": "int",
        "SystemDataId": "Lorem ipsum",
        "SystemEntityType":"EntityType"
    },    
}

然后我得到以下合理的异常抛出:

Exception Type: Newtonsoft.Json.JsonReaderException
Message: "Error reading string. Unexpected token: Undefined. Path 'ResourceKey.SystemId', line 4, position 24."
Stack Trace: " at Newtonsoft.Json.JsonReader.ReadAsStringInternal() 
at Newtonsoft.Json.JsonTextReader.ReadAsString() 
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter) 
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)"

SO:Newtonsoft.Json 库中发生了什么,导致看起来像部分 JSON 验证???

PS:可以将 JSON 名称/值对发布到 Web API,而无需将名称括在引号中...

{
    IsPublic: true, 
    ResourceKey:{       
        SystemId: "123",
        SystemDataIdType: "int",
        SystemDataId: "Lorem ipsum",
        SystemEntityType:"EntityType"
    },    
}

这也是无效的 JSON!

【问题讨论】:

  • 我能够通过一个大大简化的示例来复制这个问题 - pastebin.com/ehDgWQBu - 输出是:200 - OK - 400 - BadRequest - {"Message":"Model data is null,但它并没有通过验证!”} 我最近在另一个项目中遇到了这个问题,并认为它是我当时正在使用的自定义媒体类型格式化程序的奇怪之处,但这个示例仅使用标准格式化程序并展示了这个问题,所以我很好奇这一切意味着什么......
  • 我认为这个谜语的答案在默认模型绑定或参数绑定配置中,甚至可能在媒体类型格式化程序 (JsonMediaTypeFormatter) 中。

标签: asp.net-web-api json.net


【解决方案1】:

好的——看来问题的一部分是我自己的行为造成的。

我在控制器上有两个过滤器:

  1. 检查是否有任何 null 操作参数传递给操作方法,如果是,则返回“400 Bad Request”响应,规定参数不能为 null。
  2. ModelState 检查过滤器检查 ModelState 的错误,如果发现任何错误,则在“400 Bad Request”响应中返回它们。

我犯的错误是将空参数过滤器放在模型状态检查过滤器之前。

在模型绑定之后,第一个 JSON 示例的序列化将正确失败,并将相关的序列化异常放入 ModelState 中,并且 action 参数将保持为空,这是理所当然的。

但是,由于第一个过滤器正在检查空参数,然后返回“404 Bad Request”响应,因此 ModelState 过滤器从未启动...

因此似乎没有进行验证,而实际上确实进行了,但结果被忽略了!

重要提示:在模型绑定期间发生的序列化异常被放置在 ModelState KeyValue 对值的“异常”属性中...而不是在 ErrorMessage 属性中!

为了帮助其他人区分这种情况,这是我的 ModelValidationFilterAttribute:

public class ModelValidationFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (actionContext.ModelState.IsValid) return;

        // Return the validation errors in the response body.
        var errors = new Dictionary<string, IEnumerable<string>>();
        foreach (KeyValuePair<string, ModelState> keyValue in actionContext.ModelState)
        {
            var modelErrors = keyValue.Value.Errors.Where(e => e.ErrorMessage != string.Empty).Select(e => e.ErrorMessage).ToList();
            if (modelErrors.Count > 0)
                errors[keyValue.Key] = modelErrors;

            // Add details of any Serialization exceptions as well
            var modelExceptions = keyValue.Value.Errors.Where(e => e.Exception != null).Select(e => e.Exception.Message).ToList();
            if (modelExceptions.Count > 0)
                errors[keyValue.Key + "_exception"] = modelExceptions;
        }
        actionContext.Response =
            actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, errors);
    }
}

这里是动作方法,过滤器的顺序正确:

    [ModelValidationFilter]
    [ActionArgNotNullFilter]
    public HttpResponseMessage PostResource(Resource resource)

所以现在,以下 JSON 结果:

{
    "IsPublic": invalidvalue,   
    "ResourceKey":{     
        "SystemId": "asdf",
        "SystemDataIdType": "int",
        "SystemDataId": "Lorem ipsum",
        "SystemEntityType":"EntityType"
    },    
} 

{
    "resource.IsPublic_exception": [(2)
    "Unexpected character encountered while parsing value: i. Path 'IsPublic', line 2, position 21.",
    "Unexpected character encountered while parsing value: i. Path 'IsPublic', line 2, position 21."
    ]-
}

但是,所有这些都不能解释为什么无效的 JSON 仍然被 JsonMediaTypeFormatter 解析,例如它不要求名称是字符串。

【讨论】:

  • 作为参考,404 是“未找到”。您应该将 400 用于“错误请求”-en.wikipedia.org/wiki/…
  • @Snixtor:正确。抱歉,“404”是一个错字。我确实返回了 400。
  • 相同的异常被添加在响应中出现两次。
【解决方案2】:

更多的是一种解决方法而不是答案,但我能够使用http://aspnetwebstack.codeplex.com/workitem/609 上发布的解决方法来解决这个问题。基本上,不要让 Post 方法的签名采用 Resource 实例,而是使其不采用任何参数,然后使用 JSon.Net(或 JsonMediaTypeFormatter 的新实例)进行反序列化。

public void Post()
{
    var json = Request.Content.ReadAsStringAsync().Result;
    var resource = Newtonsoft.Json.JsonConvert.DeserializeObject<Resource>(json);

    //Important world saving work going on here
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-11-17
    • 2018-09-04
    • 1970-01-01
    • 2021-12-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多