25

I have a controller action which is to receive an integer and an object, containing various properties, one of which is a generic list of objects. When I post JSON to the action with a populated list, everything maps correctly and I get a list containing the object that I have posted. If the array is empty however, the MVC action binds the property to a null intead of an empty list. I want the empty array to map to an empty array and not to a null, as the empty array in this case means that there is nothing in the collection, and a null means that the database should be checked to see if there is anything previously saved in the collection, but I can't figure out what I need to change to get it to map properly. We are using Json.Net to do object serialization for returning objects, but I don't think it's being used for object deserialization on model binding.

Objects being passed:

public class ObjectInList
{
    public decimal Value1 { get; set; }
    public decimal Value2 { get; set; }
}

public class Criteria
{
    public decimal? ANullableNumber { get; set; }
    public IList<ObjectInList> ObjectsList { get; set; }
}

Json request: "{\"id\":137,\"criteria\":{\"ObjectsList\":[]}}"

Controller Action:

public ActionResult ProcessCriteria(int id, Criteria criteria)
{
    return Json(_service.ProcessCriteria(id, criteria));
}

It is in the controller action that I am getting a null instead of an empty list in the criteria object. It happens whether I send nulls for the other properties or not. Not sure if it's down to the object being an IList and not an IEnumerable? (The Json method wrapping the service call is our wrapper to return a json result using Json.Net to serialise the response - the null is in the criteria object received, not in the return.)

I'm guessing it's something pretty simple that I'm missing, but I can't work out what, any help greatly appreciated.

Ian Cotterill
  • 1,667
  • 4
  • 18
  • 28
  • Is easier to us if you put some code. One line of code is better than 100 words :D – Pablo Claus Jan 23 '13 at 11:52
  • Agreed, however in this case, I'm not sure how much it'll help, short of posting my entire controller/base controller and associated json posts. I'll try and post something that makes some sense, but until then, the json is showing an empty array, and before it's posted and when it's bound to the model, the List is a null rather than an empty list. Parts of the controller hierarchy were written by other devs, so I can't say for sure, but I can't find an overridden implementation of the json deserialisation, so I think it's the default handling of json to model binding. – Ian Cotterill Jan 23 '13 at 12:01
  • Check this question: http://stackoverflow.com/q/14203150/29555 and the second answer – marto Jan 23 '13 at 12:12
  • @marto thanks, I've had a look, not sure how it helps - I can't post process the collection as in this situation a null object and an empty collection have different meanings. The action method is on an API, and the post to it is being generated from .Net, not from JQuery - the posted Json request is serialised by Json.Net, and I can watch it leave the client correctly, it's when checking the model in debug as it arrives in the controller that I find I have a null model instead of an empty collection. – Ian Cotterill Jan 23 '13 at 12:19
  • Sorry for the wrong link. Have you tried to create an empty contstructor in the Criteria class that initialises the list. If that doesn't work then add a backing field to the property and on the setter check if the value is null and set the internal field to the an empty list – marto Jan 23 '13 at 12:47
  • Not sure how that will work - I want to be able to send a null as well as sending an empty list and have different behaviour dependant on which I send. If I add a backing field then will it actually set the list as null when it should be and if it does, how will that change the behaviour if the model is being bound to a null? I've also just experimented with using an IEnumerable and just an array instead of an IList and neither make any difference. – Ian Cotterill Jan 23 '13 at 12:56

6 Answers6

12

ok, i was facing this issue almost 5 hours trying find the solution then i found myself looking in the MVC source code. and i found that this is a problem with the Mvc Source code in System.Web.Mvc.ValueProviderResult at Line 173:

        else if (valueAsArray != null)
        {
            // case 3: destination type is single element but source is array, so                     extract first element + convert
            if (valueAsArray.Length > 0)
            {
                value = valueAsArray.GetValue(0);
                return ConvertSimpleType(culture, value, destinationType);
            }
            else
            {
                // case 3(a): source is empty array, so can't perform conversion
                return null;
            }
        }

as you can see if source is empty array it will return null.

so i have to find a way around it, and then i remember how in the good old days we was doing deserialization: this is how you will get what you want:

    public ActionResult ProcessCriteria(int id, Criteria criteria)
    {
        var ser = new System.Web.Script.Serialization.JavaScriptSerializer();
        StreamReader reader = new StreamReader(System.Web.HttpContext.Current.Request.InputStream);
        reader.BaseStream.Position = 0;
        criteria = ser.Deserialize<Criteria>(reader.ReadToEnd());

        return Json(_service.ProcessCriteria(id, criteria));
    }
ygaradon
  • 2,198
  • 2
  • 21
  • 27
  • 2
    This is an obvious bug, i think. You can post issue or make commit there http://aspnetwebstack.codeplex.com/ – Arman Hayots Apr 06 '15 at 15:04
  • 1
    I would think the actual problem is in DefaultModelBinder https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/src/System.Web.Mvc/DefaultModelBinder.cs line 711 where it returns null if the built `objectList` contains nothing. Check this out: https://lostechies.com/jimmybogard/2013/11/07/null-collectionsarrays-from-mvc-model-binding/ – bigbearzhu Jan 30 '17 at 02:39
  • I used finally had to use Newtonsoft to deserialize the posted data `JsonConvert.DeserializeObject((new StreamReader(Request.InputStream)).ReadToEnd()) ` – Yashash Gaurav Jun 30 '20 at 13:44
3

One way of resolving this issue is assigning a new instance as a default value for your ObjectsList like this:

public class Criteria
{
    public decimal? ANullableNumber { get; set; }
    public IList<ObjectInList> ObjectsList { get; set; } = new List<ObjectInList>();
}

This will create an empty List instead of null if there's no values in your JSON array.

krlzlx
  • 5,752
  • 14
  • 47
  • 55
1

I have an answer for you that will work at the framework level. In my project, I was working with data that was a bit larger than the default values would support. Thus, I created my own ValueProviderFactory. It turns out, if an array has no items in it, the provider skipped over that entry entirely. Instead, we just have to tell it that no items are in the array. Here is the code you will need.

First, the global.asax Application_Start:

public void Application_Start()
{
    ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<System.Web.Mvc.JsonValueProviderFactory>().FirstOrDefault());
    ValueProviderFactories.Factories.Add(new LargeValueProviderFactory());

Second, here is the other class you will need:

using System;
using System.Collections.Generic;
using System.Collections;
using System.Web.Mvc;
using System.IO;
using System.Web.Script.Serialization;
using System.Globalization;

public sealed class LargeValueProviderFactory : System.Web.Mvc.ValueProviderFactory
{    
    public override System.Web.Mvc.IValueProvider GetValueProvider(ControllerContext controllerContext)
    {
        if (controllerContext == null)
        {
            throw new ArgumentNullException("controllerContext");
        }

        object jsonData = GetDeserializedObject(controllerContext);
        if (jsonData == null)
        {
            return null;
        }

        Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
        AddToBackingStore(backingStore, String.Empty, jsonData);
        return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
    }

    private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
    {
        IDictionary<string, object> d = value as IDictionary<string, object>;
        if (d != null)
        {
            foreach (KeyValuePair<string, object> entry in d)
            {
                AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
            }
            return;
        }

        IList l = value as IList;
        if (l != null)
        {
            for (int i = 0; i < l.Count; i++)
            {
                AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
            }
            if (l.Count == 0)
                backingStore[prefix] = value;
            return;
        }

        // primitive
        backingStore[prefix] = value;
    }

    private static object GetDeserializedObject(ControllerContext controllerContext)
    {
        
        if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
        {
            // not JSON request
            return null;
        }

        StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
        string bodyText = reader.ReadToEnd();
        if (String.IsNullOrEmpty(bodyText))
        {
            // no JSON data
            return null;
        }

        JavaScriptSerializer serializer = new JavaScriptSerializer();
        serializer.MaxJsonLength = Int32.MaxValue;
        object jsonData = serializer.DeserializeObject(bodyText);
        return jsonData;
    }

    private static string MakeArrayKey(string prefix, int index)
    {
        return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
    }

    private static string MakePropertyKey(string prefix, string propertyName)
    {
        return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
    }
}
Daniel Lorenz
  • 4,178
  • 1
  • 32
  • 39
1

I would think the actual problem is in DefaultModelBinder.cs line 711 where it returns null if the built objectList contains nothing. Check this out: https://lostechies.com/jimmybogard/2013/11/07/null-collectionsarrays-from-mvc-model-binding/

bigbearzhu
  • 2,381
  • 6
  • 29
  • 44
0

Here is what I posted as a comment:

public class Criteria
{
    public decimal? ANullableNumber { get; set; }
    private IList<ObjectInList> _objectsList = new List<ObjectInList>();
    public IList<ObjectInList> ObjectsList 
    { 
        get { return _objectsList; } 
        set { 
            if(value != null) 
                _objectsList = value;
        }
     }
}
marto
  • 4,170
  • 1
  • 28
  • 39
  • 2
    As I said in my comment above, I want to be able to have a null state for this list as well, so if I do try and send a null in this case, it will still show it as an empty list - which is not the desired behaviour. – Ian Cotterill Jan 23 '13 at 12:57
-2

This is because your never define nullable properties value in 'Criteria' class; if never define, it will be null.

for example:

  public class Criteria {
    public decimal? ANullableNumber { get; set; }
    public IList<ObjectInList> ObjectsList { get; set; }
  }
  public class Criteria1 {
    private IList<ObjectInList> _ls;
    private decimal? _num;
    public decimal? ANullableNumber {
      get {
        if (_num == null) return 0;
        return _num;
      }
      set {
        _num = value;
      }
    }
    public IList<ObjectInList> ObjectsList {
      get {
        if (_ls == null) _ls = new List<ObjectInList>();
        return _ls;
      }
      set {
        _ls = value;
      }
    }
  }
  public class HomeController : Controller {
    public ActionResult Index() {
      var dd = new Criteria();
      return Json(dd);    //output: {"ANullableNumber":null,"ObjectsList":null}
    }
    public ActionResult Index1() {
      var dd = new Criteria1();
      return Json(dd);    //output: {"ANullableNumber":0,"ObjectsList":[]}
    }
  }
Leng Weh Seng
  • 725
  • 8
  • 6