5

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!

Community
  • 1
  • 1
JTech
  • 3,420
  • 7
  • 44
  • 51
  • I was able to replicate this issue with a greatly simplified example - http://pastebin.com/ehDgWQBu - The output from this is: 200 - OK - 400 - BadRequest - {"Message":"Model data is null, but it didn't fail validation!"} I encountered this issue myself recently in another project and dismissed it as an oddity of a custom media type formatter I was working with at the time, but this example uses only standard formatters and exhibits the issue, so I'm curious what it all means... – Snixtor Feb 27 '13 at 03:15
  • I'm thinking the answer to this riddle lies somewhere in the default model binding or parameter binding configuration, possibly even in the media type formatter (`JsonMediaTypeFormatter`). – Snixtor Feb 27 '13 at 03:49

2 Answers2

3

OK - so it appears that part of the problem was caused by my own doing.

I had two filters on the controller:

  1. Checks whether there are any null action parameters being passed to an action method and if so, returns a "400 Bad Request" response stipulating that the parameter cannot be null.
  2. A ModelState inspection filter which checked the Errors of the ModelState and if any are found, return them in a "400 Bad Request" response.

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.

JTech
  • 3,420
  • 7
  • 44
  • 51
1

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
}
Greg Biles
  • 941
  • 2
  • 10
  • 10