Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebAPI instantiating objects even if they are null in JSON

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?

like image 996
Casey Williams Avatar asked Oct 19 '22 18:10

Casey Williams


1 Answers

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.

like image 153
Maxim Avatar answered Nov 15 '22 09:11

Maxim