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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With