5

When posting JSON models to WebAPI controller methods I've noticed that if there are null objects in the JSON model the model binder will instantiate these items instead of keeping them null in the server side object.

This is in contrast to how a normal MVC controller would bind the data...it would not instantiate an object if it is null in the JSON.

MVC Controller

public class HomeController : Controller
{
    [HttpPost]
    public ActionResult Test(Model model)
    {
        return Json(model);
    }
}

WebAPI Controller

public class APIController : ApiController
{
    [HttpPost]
    public Model Test(Model model)
    {
        return model;
    }
}

Model class that will get POSTed

public class Model
{
    public int ID { get; set; }
    public Widget MyWidget { get; set; }
}

Class used in the Model class

public class Widget
{
    public int ID { get; set; }
    public string Name { get; set; }
}

Here are my results when I post a JSON Model to each controller:

$.post('/Home/Test', { ID: 29, MyWidget: null })
//Results in: {"ID":29,"MyWidget":null}

$.post('/api/api/Test', { ID: 29, MyWidget: null })
//Results in: {"ID":29,"MyWidget":{"ID":0,"Name":null}}

As you can see, the WebAPI method instantiated the MyWidget property with an object, whereas the MVC action left it null.

It doesn't seem intuitive to me that WebAPI would function this way. Why would it do this? Can I make it behave like an MVC action in this regard?

4
  • There's no chance that your Model constructs a Widget in its default constructor and assigns it to MyWidget, is there? Commented Jan 5, 2016 at 22:44
  • What version of Web API are you using? Commented Jan 5, 2016 at 23:03
  • @dbc, no the class is exactly as you see it. No constructor is specified. And besides, if it were a constructor based issue it would happen for both controllers. Commented Jan 6, 2016 at 2:25
  • @Nikolai, WebAPI version 2 Commented Jan 6, 2016 at 2:28

2 Answers 2

3

I think it is similar to issues previously experienced in our projects.

You have to change post code for jQuery to the following one:

$.ajax({
            type: 'POST',
            url: '/api/api/Test',
            data: JSON.stringify({ ID: 29, MyWidget: null }),
            contentType: "application/json",
            dataType: 'json',
            timeout: 30000
        })
        .done(function (data) {
        })
        .fail(function() {
        });

By default jQuery 'posts' sends parameters as form-url-encoded data.

application/x-www-form-urlencoded
ID 29
MyWidget

ID=29&MyWidget=

So it was deserialized absolutely correctly. MyWidget is empty string, so it will have empty value of Widget class.

In addition I recommend you to add Formatters configuration for WebApi controllers:

public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        // Formatters
        JsonMediaTypeFormatter json = config.Formatters.JsonFormatter;

        config.Formatters.Clear();
        config.Formatters.Add(json);
    }

So you will use only JSON-formatter for API calls.

UPDATE

Main difference that form-url-encoded data passed to MVC controller will be processed by runtime and finally handled by DefaultModelBinder (if custom binder is not available). So data encoded as form-url is usual for MVC because normally data is generated by HTML form post. But Web API is not rely on any specific encoding by design. So it uses specific mechanism (formatters) to parse data... for example json like it is above. So FormUrlEncodedMediaTypeFormatter from System.Net.Http.Formatting and DefaultModelBinder from System.Web.Mvc handle empty string differently.

For DefaultModelBinder empty string will be converted to null. Analyzing code I can decide that BindModel method firstly creates empty model:

 if (model == null)
 {
     model = CreateModel(controllerContext, bindingContext, modelType);
 }

After it will fill properties:

// call into the property's model binder
        IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType);
        object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model);
        ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
        propertyMetadata.Model = originalPropertyValue;
        ModelBindingContext innerBindingContext = new ModelBindingContext()
        {
            ModelMetadata = propertyMetadata,
            ModelName = fullPropertyKey,
            ModelState = bindingContext.ModelState,
            ValueProvider = bindingContext.ValueProvider
        };
        object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);

And finally GetBinder will return fallbackBinder for Widget type (type of property). And fallbackBinder itself will call ConvertSimpleType where string is processed as the following:

        string valueAsString = value as string;
        if (valueAsString != null && String.IsNullOrWhiteSpace(valueAsString))
        {
            return null;
        }

I guess there are no any standards describing conversion from url-encoded strings to C# objects. So I do not know which one is correct. In any case I am sure you need to pass json through AJAX calls not form-url-encoded data.

Sign up to request clarification or add additional context in comments.

3 Comments

That worked (specifically using $.ajax), thank you for your insight! It's interesting to me that the MVC Action and the WebAPI method behave differently in this regard.
I will add explanation regarding difference in original answer.
Excellent explanation, thank you! I wish I could up vote your answer again.
0

Use newtonsoft serializer. Newtonsoft.Json keeps them null

using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;


public static class WebApiConfig
        {
            public static void Register(HttpConfiguration config)
            {
              var jsonformatter = new JsonMediaTypeFormatter();
              config.Formatters.Clear();
              config.Formatters.Add(jsonformatter);
         }
    }

3 Comments

Doesn't Web API use Newtonsoft.Json serializer by default?
@NikolaiSamteladze you are right. I have checked now. But Json.Net doesnt initialize null objects.
The object is getting instantiated during the binding time. If I breakpoint in the controller action I can see the new object already created (or null in the case of the MVC controller).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.