I have the following model:
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; }
}
I have the following action method signature:
public HttpResponseMessage PostResource(Resource resource)
I send the following request with JSON in the body (an intentionally invalid value for property "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"
},
}
This is invalid JSON - run it through JSONLint and it tells you:
Parse error on line 2:
{ "IsPublic": invalidvalue,
.................^ Expecting 'STRING', 'NUMBER', 'NULL', 'TRUE', 'FALSE', '{', '['
The ModelState.IsValid property is 'true' - WHY???
Also, instead of throwing a validation error, the formatter seems to give up on deserializing and simply passes the 'resource' argument to the action method as null!
Note that this also happens if I put in an invalid value for other properties, e.g. substituting:
"SystemId": notAnObjectOrLiteralOrArray
However, if I send the following JSON with a special undefined value for the "SystemId" property:
{
"IsPublic": true,
ResourceKey:{
"SystemId": undefined,
"SystemDataIdType": "int",
"SystemDataId": "Lorem ipsum",
"SystemEntityType":"EntityType"
},
}
Then I get the following, reasonable, exception thrown:
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: what is going on in the Newtonsoft.Json library which results in what seems like partial JSON Validation???
PS: It is possible to post JSON name/value pairs to the Web API without enclosing the names in quotes...
{
IsPublic: true,
ResourceKey:{
SystemId: "123",
SystemDataIdType: "int",
SystemDataId: "Lorem ipsum",
SystemEntityType:"EntityType"
},
}
This is also invalid JSON!
OK - so it appears that part of the problem was caused by my own doing.
I had two filters on the controller:
The mistake I made was to put the null argument filter before the model state checking filter.
After Model Binding, the serialization would fail correctly for the first JSON example, and would put the relevant serialization exception in ModelState and the action argument would remain null, rightfully so.
However, since the first filter was checking for null arguments and then returning a "404 Bad Request" response, the ModelState filter never kicked in...
Hence it seemed that validation was not taking place, when in fact it was, but the results were being ignored!
IMPORTANT: Serialization exceptions that happen during Model Binding are placed in the 'Exception' property of the ModelState KeyValue pair Value...NOT in the ErrorMessage property!
To help others with this distinction, here is my 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);
}
}
And here is the action method, with the filters in the correct order:
[ModelValidationFilter]
[ActionArgNotNullFilter]
public HttpResponseMessage PostResource(Resource resource)
So now, the following JSON results in:
{
"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."
]-
}
However, all of this does not explain why invalid JSON is still parsed by the JsonMediaTypeFormatter e.g. it does not require that names be strings.
More of a workaround than an answer, but I was able to get this to work using the workaround posted at http://aspnetwebstack.codeplex.com/workitem/609. Basically, instead of having your Post method's signature take a Resource instance, make it take no parameters and then use JSon.Net (or a new instance of JsonMediaTypeFormatter) to do the deserialization.
public void Post()
{
var json = Request.Content.ReadAsStringAsync().Result;
var resource = Newtonsoft.Json.JsonConvert.DeserializeObject<Resource>(json);
//Important world saving work going on here
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With