Thursday, January 23, 2014

Default model binder deserializes json array with single element as null in ASP.Net MVC

Some time ago I faced with strange problem. Suppose that we have POCO which is used as action post parameter:

   1: public class RequestWrapper
   2: {
   3:     public Request request { get; set; }
   4: }
   5:  
   6: public class Request
   7: {
   8:     public string id { get; set; }
   9:     public string status { get; set; }
  10:     public string notes { get; set; }
  11:     ... // a lot of other integer and string fields
  12:     public RequestItem[] items { get; set; }
  13: }
  14:  
  15: public class RequestItem
  16: {
  17:     public int id { get; set; }
  18:     public string title { get; set; }
  19:     ... // a lot of other integer and string fields
  20: }

Class structure is quite straightforward, all properties are public and have getter and setter. As you can see Request contains array of RequestItems objects. And it is used as parameter of controller’s post action:

   1: [HttpPost]
   2: public ActionResult Foo(RequestWrapper request)
   3: {
   4:     ...
   5: }

The following json object is successfully passed to this action and deserialized with default model binder:

   1: {
   2:    "request":{
   3:       "id":"1",
   4:       "status":"TEST",
   5:       "notes":"",
   6:       ...
   7:       "items":[
   8:          {
   9:             "id":"2",
  10:             "title":"Test1",
  11:             ...
  12:          },
  13:          {
  14:             "id":"3",
  15:             "title":"Test2",
  16:             ...
  17:          }
  18:       ]
  19:    }
  20: }

The problems however begin when items array contains only 1 element:

   1: {
   2:    "request":{
   3:       "id":"1",
   4:       "status":"TEST",
   5:       "notes":"",
   6:       ...
   7:       "items":[
   8:          {
   9:             "id":"2",
  10:             "title":"Test1",
  11:             ...
  12:          }
  13:       ]
  14:    }
  15: }

In this case default model binder successfully deserialized all properties of Request object except items, which was set to null. Adding more items caused deserialization to work properly, i.e. items array was not empty in this case. I didn’t find mentions of this problem on forums and blogs. The only thing which I noticed that many people faced with unstable work of default model binder when json is deserialized. One of the solution was to create custom model binder and replace default deserializer with JSON.Net library. I tried first standard JavaScriptSerializer from System.Web.Extensions assembly. I.e. create custom model binder:

   1:  
   2: public class RequestWrapperModelBinder : DefaultModelBinder
   3: {
   4:     public override object BindModel(ControllerContext controllerContext,
   5:         ModelBindingContext bindingContext)
   6:     {
   7:         try
   8:         {
   9:             var httpRequest = controllerContext.HttpContext.Request;
  10:             httpRequest.InputStream.Position = 0;
  11:             using (var sr = new StreamReader(httpRequest.InputStream))
  12:             {
  13:                 var str = sr.ReadToEnd();
  14:                 var request =
  15:                     new JavaScriptSerializer().Deserialize<RequestWrapper>(str);
  16:                 return request;
  17:             }
  18:         }
  19:         catch
  20:         {
  21:             return base.BindModel(controllerContext, bindingContext)
  22:                 as RequestWrapper;
  23:         }
  24:     }
  25: }

As you can see it first reads the content of post request from InputStream and tries to deserialize it via JavaScriptSerializer. If it fails and exception is thrown, it still uses default model binder. And this approach worked: i.e. array with single element became deserialized properly.

And the last thing which is needed is to register custom model binder:

   1: ModelBinders.Binders.Add(typeof(RequestWrapper), new RequestWrapperModelBinder());

However the original problem is still unclear. Need to mention that it is specific to this class only. There were another similar classes with arrays (they were more simple, i.e. contained less properties) and they were deserialized by default model binder successfully when array contained single element. So if you faced with this problem and know another solution, please share it in comments.

No comments:

Post a Comment