This may sound counter-intuitive, but you should send these doubles as strings in json:
'data':{'d1':'2','d2':null,'d3':'2'}
Here is my complete test code that invokes this controller action using AJAX and allows you to bind to all model values:
$.ajax({ url: '@Url.Action("save", new { id = 123 })', type: 'POST', contentType: 'application/json', data: JSON.stringify({ items: [ { sid: 3157, name: 'a name', items: [ { sid: 3158, name: 'child name', data: { d1: "2", d2: null, d3: "2" } } ] } ] }), success: function (result) { // ... } });
And just to illustrate the extent of the problem of deserializing numeric types from JSON, let's look at a few examples:
public double? Foo { get; set; }{ foo: 2 } => Foo = null{ foo: 2.0 } => Foo = null{ foo: 2.5 } => Foo = null{ foo: '2.5' } => Foo = 2.5
public float? Foo { get; set; }{ foo: 2 } => Foo = null{ foo: 2.0 } => Foo = null{ foo: 2.5 } => Foo = null{ foo: '2.5' } => Foo = 2.5
public decimal? Foo { get; set; }{ foo: 2 } => Foo = null{ foo: 2.0 } => Foo = null{ foo: 2.5 } => Foo = 2.5{ foo: '2.5' } => Foo = 2.5
Now let's do the same with non-nullable types:
public double Foo { get; set; }{ foo: 2 } => Foo = 2.0{ foo: 2.0 } => Foo = 2.0{ foo: 2.5 } => Foo = 2.5{ foo: '2.5' } => Foo = 2.5
public float Foo { get; set; }{ foo: 2 } => Foo = 2.0{ foo: 2.0 } => Foo = 2.0{ foo: 2.5 } => Foo = 2.5{ foo: '2.5' } => Foo = 2.5
public decimal Foo { get; set; }{ foo: 2 } => Foo = 0{ foo: 2.0 } => Foo = 0{ foo: 2.5 } => Foo = 2.5{ foo: '2.5' } => Foo = 2.5
Conclusion: Deserializing numeric types from JSON is one big hell of a mess. Use strings in JSON. And of course, when you use strings, be careful with the decimal separator, as it depends on the culture.
In the comments section, I asked why this passes unit tests, but does not work in ASP.NET MVC. The answer is simple: this is because ASP.NET MVC does much more than a simple JavaScriptSerializer.Deserialize call, which the unit test does. So you basically compare apples to oranges.
Let go deeper into what is happening. ASP.NET MVC 3 has a built-in JsonValueProviderFactory that internally uses the JavaScriptDeserializer class to deserialize JSON. This works, as you have already seen, in unit test. But ASP.NET MVC is much more, since it also uses the default middleware, which is responsible for instantiating your action parameters.
And if you look at the source code for ASP.NET MVC 3 and, more specifically, the DefaultModelBinder.cs class, you will see the following method that is called for each property that will have a value that needs to be set:
public class DefaultModelBinder : IModelBinder { ............... [SuppressMessage("Microsoft.Globalization", "CA1304:SpecifyCultureInfo", MessageId = "System.Web.Mvc.ValueProviderResult.ConvertTo(System.Type)", Justification = "The target object should make the correct culture determination, not this method.")] [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We're recording this exception so that we can act on it later.")] private static object ConvertProviderResult(ModelStateDictionary modelState, string modelStateKey, ValueProviderResult valueProviderResult, Type destinationType) { try { object convertedValue = valueProviderResult.ConvertTo(destinationType); return convertedValue; } catch (Exception ex) { modelState.AddModelError(modelStateKey, ex); return null; } } ............... }
Focus more specifically on the following line:
object convertedValue = valueProviderResult.ConvertTo(destinationType);
If we assume that you have a property of type Nullable<double> , here is what it will look like when debugging your application:
destinationType = typeof(double?);
No surprises here. Our destination type is double? , because this is what we used in our viewing model.
Then take a look at valueProviderResult :

See this RawValue property there? Can you guess his type?

So this method just throws an exception because it obviously cannot convert the value of decimal 2.5 to double? .
Have you noticed what value is returned in this case? This is why you end up with null in your model.
This is very easy to verify. Just check the ModelState.IsValid property inside your controller action and you will notice that it is false . And when you check the model error added to the model state, you will see the following:
The conversion of parameters from the type "System.Decimal" to the type 'System.Nullable`1 [[System.Double, mscorlib, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089]]' failed, because no type converter can convert between these types.
Now you may ask: "But why is the RawValue property inside a ValueProviderResult of type decimal?" Once again, the answer lies inside the ASP.NET MVC 3 source code (yes, you should have downloaded it by now). Let's look at the JsonValueProviderFactory.cs file and, more specifically, the GetDeserializedObject method:
public sealed class JsonValueProviderFactory : ValueProviderFactory { ............ 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(); object jsonData = serializer.DeserializeObject(bodyText); return jsonData; } ............ }
You notice the following line:
JavaScriptSerializer serializer = new JavaScriptSerializer(); object jsonData = serializer.DeserializeObject(bodyText);
Can you guess that the next snippet will print on your console?
var serializer = new JavaScriptSerializer(); var jsonData = (IDictionary<string, object>)serializer .DeserializeObject("{\"foo\":2.5}"); Console.WriteLine(jsonData["foo"].GetType());
Yes, you guessed it right, it is decimal .
Now you may ask: "But why did they use the serializer.DeserializeObject method instead of serializer.Deserialize, as in my unit test?"? This is because the ASP.NET MVC team made a design decision to implement JSON request binding using ValueProviderFactory , which does not know the type of your model.
See how your unit test is completely different from what is really happening under ASP.NET MVC 3 covers? What usually should explain why it passes, and why the controller’s action does not get the correct model value?