40

Here is my model class:

public class MyModel
{
    public Employees[] MyEmpls{get;set;}
    public int Id{get;set;}
    public OrgName{get;set;}
}

Passing the below JSON structure object with MyEmpls as empty array to MVC controller.

["Id":12, "MyEmpls":[], "OrgName":"Kekran Mcran"]

Controller

[HttpPost]
public ActionResult SaveOrg(MyModel model)
{
  //model.MyEmpls is null here
}

I am expecting mode.MyEmpls to be an empty c# array, not a null. Is a custom model binder necessary to achieve an empty array?

Derek Greer
  • 15,454
  • 5
  • 45
  • 52
Billa
  • 5,226
  • 23
  • 61
  • 105

7 Answers7

37

I think that some of the other answers have missed the meaning of the question: why does the default MVC model binder bind an empty Json array to null instead of an empty C# array?

Well, I can't tell you why they did that, but I can show you where it happens. The source for MVC can be found on CodePlex here: http://aspnetwebstack.codeplex.com/SourceControl/latest. The file you're looking for is ValueProviderResult.cs where you can see:

    private static object UnwrapPossibleArrayType(CultureInfo culture, object value, Type destinationType)
    {
        if (value == null || destinationType.IsInstanceOfType(value))
        {
            return value;
        }

        // array conversion results in four cases, as below
        Array valueAsArray = value as Array;
        if (destinationType.IsArray)
        {
            Type destinationElementType = destinationType.GetElementType();
            if (valueAsArray != null)
            {
                // case 1: both destination + source type are arrays, so convert each element
                IList converted = Array.CreateInstance(destinationElementType, valueAsArray.Length);
                for (int i = 0; i < valueAsArray.Length; i++)
                {
                    converted[i] = ConvertSimpleType(culture, valueAsArray.GetValue(i), destinationElementType);
                }
                return converted;
            }
            else
            {
                // case 2: destination type is array but source is single element, so wrap element in array + convert
                object element = ConvertSimpleType(culture, value, destinationElementType);
                IList converted = Array.CreateInstance(destinationElementType, 1);
                converted[0] = element;
                return converted;
            }
        }
        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;
            }
        }
        // case 4: both destination + source type are single elements, so convert
        return ConvertSimpleType(culture, value, destinationType);
    }
}

The interesting part is "case 3":

else
{
    // case 3(a): source is empty array, so can't perform conversion
    return null;
}

You can sidestep this issue by initialising your array on the model in its constructor. In my quick reading of the source I can't tell you why they can't return an empty array or why they decide not to, but it should make for interesting reading.

Richiban
  • 5,569
  • 3
  • 30
  • 42
  • 1
    It's seems the issue is that `destinationType.IsArray` returns false for `List<>` class, where it would work for Array type. – ghord May 02 '14 at 08:54
  • In the code the OP posted he *has* used Employee[] rather than List, so unless that's a mistake I'm at a loss... – Richiban May 06 '14 at 08:48
  • 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:34
  • Is there a way to override the definition of DefaultModelBinder, so that an Empty Array is retained as an Empty Array after deserialization ? The idea is that my Controller method is associated with a HTTPPatch and this specific array is expected to be empty, for it to be processed. – Rohit V Mar 02 '17 at 23:13
  • 2
    Note that in MVC Core this behavior has been changed to the expected behavior of empty JSON arrays being bound to an empty collection. – Derek Greer Jun 27 '19 at 13:49
  • 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:42
  • "You can sidestep this issue by initialising your array on the model in its constructor" Nope, does not work for me; the entire model is null, not just the property, the whole thing. Seems daft, but cant find a workaround right now except, ignoring validation and handling the null value downstream – Dan Jul 25 '23 at 13:34
30

You're getting a null value as this is the default value for a reference type in C#. In order to get an empty array you will need to initialise the array in your model using a constructor. However as you will need to define the size of the array when it's initialized it might be better using another type of collection such as a List:

public class MyModel
{
    public List<Employees> MyEmpls{get;set;}
    public int Id{get;set;}
    public OrgName{get;set;}

    public MyModel() 
    {
         MyEmpls = new List<Employees>();
    }
}

You will then get an empty list when an empty array is passed from the json.

If you really have to use an array just initialise it with a size:

public class MyModel
{
    public Employees[] MyEmpls{get;set;}
    public int Id{get;set;}
    public OrgName{get;set;}

    public MyModel() 
    {
         MyEmpls = new Employees[/*enter size of array in here*/];
    }
}
James
  • 2,195
  • 1
  • 19
  • 22
  • It is a WCF proxy class that is being used as `MyModel`. So i think he might not have control to change `Employee[]` to `List` – HCJ May 02 '14 at 07:09
  • 2
    Null and empty mean very different things. In my case - specifying changes to a model. Null means ignore this property, leave it as it is. Empty means remove all elements. – Julian Mann Feb 12 '16 at 03:30
0

If you are getting model.MyEmpls as null then you can create a condition on server side to stop raising exception like:

if(model.MyEmpls !=null){
...
}

And you are getting it null because your MyEmpls is a Custom Class array and you are sending just [].

Hope this helps you.

Mehmood
  • 933
  • 5
  • 17
0

Try to create a model binder as in below

public class MyModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        try
        {                
            var request = controllerContext.HttpContext.Request;

            return new MyModel
            {
                MyEmpls = request[] ?? new Employees[0],
                Id = request["Id"] ?? "",
                OrgName = request["OrgName"] ?? ""

            };
        }
        catch 
        {
            //do required exception handling
        }
    }
}

In Application_Start register the model binder

ModelBinders.Binders.Add(typeof(MyModel), new MyModelBinder())

And modify the controller as

[HttpPost]
public ActionResult SaveOrg([ModelBinder(typeof(MyModelBinder))] MyModel model)
{
  //model.MyEmpls is null here
}
HCJ
  • 529
  • 1
  • 6
  • 14
0

you can define a setter that checks if the value is null

public class MyModel
{
    private _myEmpls{get;set;}
    public Employees[] MyEmpls{
     get{return _myEmpls;}
     set{_myEmpls=(value==null?new List<Employees>():value);}
    }

    public int Id{get;set;}
    public OrgName{get;set;}
}
hmd.ai
  • 1,163
  • 11
  • 13
0
[HttpPost]
public ActionResult SaveOrg(MyModel model)
{
    var serializer = new JavaScriptSerializer();
    var stream = System.Web.HttpContext.Current.Request.InputStream;
    var reader = new StreamReader(stream);
    stream.Position = 0;
    var json = reader.ReadToEnd();
    model= serializer.Deserialize<MyModel>(json);
    //model.MyEmpls is [] here
}

JavaScriptSerializer deserializes empty arrays properly. So you can ignore the passed-in model and rebuild it from the input request stream. Probably not the correct way, but if you only need to do it once it saves some effort. You'll need to reference System.Web.Extensions.

Julian Mann
  • 6,256
  • 5
  • 31
  • 43
-1

The default behavior of C# array initialization is to null not empty array

So if you send an empty array you will not initiate an empty array, but you will trigger the default initialization to null

See the following link http://www.dotnetperls.com/null-array

Kushan Randima
  • 2,174
  • 5
  • 31
  • 58
stackunderflow
  • 3,811
  • 5
  • 31
  • 43
  • I think the issue the question was getting at was that `[]` in JSON was deserialised as `null` it C# rather than `new [] {}`. This behaviour seems strange to me since I consider an empty C# array to be closer to a JSON array than `null`. – Sam Dec 07 '15 at 22:29