简体   繁体   中英

MVC3 RTM fails number type coercion when deserializing JSON

Simply put serializing data in the "application/json; charset=utf-8" format misbehaves in MVC3 (and possibly older versions). What happens is a nullable numbers all end up null, and numbers of "decimal" type end up 0 when serializing them inside a javascript object (to JSON) and leaving them as numbers and not strings.

Here is the example code that illustrates this misbehavior
- - - this example was created using jquery-1.4.4.js and jquery.json-2.2.js - - - -

HomeController.cs:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        ViewBag.SaveUrl = Url.Action("Save", "Home", new { inspectionFormID = Guid.Empty }, Request.Url.Scheme);
        return View();
    }

    public JsonResult Save(Guid inspectionFormID, JsonTest result)
    {
        return Json(result);
    }

    public class JsonTest 
    {
        public double Double { get; set; }
        public double? DoubleNull { get; set; }

        public decimal Decimal { get; set; }
        public decimal? DecimalNull { get; set; }

        public Double Double2 { get; set; }
        public Double? Double2Null { get; set; }

        public Decimal Decimal2 { get; set; }
        public Decimal? Decimal2Null { get; set; }

        public Single Single { get; set; }
        public Single? SingleNull { get; set; }

        public float Float { get; set; }
        public float? FloatNull { get; set; }

        public int Int { get; set; }
        public int? IntNull { get; set; }

        public Int64 Int64 { get; set; }
        public Int64? Int64Null { get; set; }
    }

}

Index.cshtml:

    @{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<b>@ViewBag.SaveUrl</b>
<br />
<hr />
<br />

<h3>Integral Numbers</h3>
<button type="button" class="a">Clicky</button>
<div></div>

<h3>Decimal Numbers (xx.0)</h3>
<button type="button" class="b">Clicky</button>
<div></div>

<h3>Decimal Numbers (xx.5)</h3>
<button type="button" class="c">Clicky</button>
<div></div>

<h3>Integral Numbers as strings</h3>
<button type="button" class="d">Clicky</button>
<div></div>

<h3>Decimal Numbers as strings (xx.5)</h3>
<button type="button" class="e">Clicky</button>
<div></div>

<script type="text/javascript">

    $(function () {
        var saveUrl = '@ViewBag.SaveUrl';

        var printObj = function (inObj, destx) {
            var dest = $('<table>').appendTo(destx),
                dst1 = $('<tr>').appendTo(dest),
                dst2 = $('<tr>').appendTo(dest);
            for (var p in inObj) {
                $('<th>', { text: p, css: { color: 'red', padding: '3px', background: '#dedede' } }).appendTo(dst1);
                $('<td>', { text: inObj[p] || 'null' }).appendTo(dst2);
            }
        };

        $('button.a').click(function () {
            var curr = $(this).next(),
                outR = {
                    Double: 12,
                    DoubleNull: 13,
                    Decimal: 14,
                    DecimalNull: 15,
                    Double2: 16,
                    Double2Null: 17,
                    Decimal2: 18,
                    Decimal2Null: 19,
                    Single: 20,
                    SingleNull: 21,
                    Float: 22,
                    FloatNull: 23,
                    Int: 24,
                    IntNull: 25,
                    Int64: 26,
                    Int64Null: 27
                };

            $('<hr />').appendTo(curr);
            printObj(outR, curr);

            $.ajax({
                type: 'POST',
                url: saveUrl,
                contentType: "application/json; charset=utf-8",
                dataType: 'json',

                data: $.toJSON({
                    inspectionFormID: 'fbde6eda-dde6-4ba9-b82d-3a35349415f0',

                    result: outR
                }),

                error: function (jqXHR, textStatus, errorThrown) {
                    alert('save failed');
                },

                success: function (data, textStatus, jqXHR) {
                    printObj(data, curr);
                }
            });
        });

        $('button.b').click(function () {
            var curr = $(this).next(),
                outR = {
                    Double: 12.0,
                    DoubleNull: 13.0,
                    Decimal: 14.0,
                    DecimalNull: 15.0,
                    Double2: 16.0,
                    Double2Null: 17.0,
                    Decimal2: 18.0,
                    Decimal2Null: 19.0,
                    Single: 20.0,
                    SingleNull: 21.0,
                    Float: 22.0,
                    FloatNull: 23.0,
                    Int: 24.0,
                    IntNull: 25.0,
                    Int64: 26.0,
                    Int64Null: 27.0
                };

            $('<hr />').appendTo(curr);
            printObj(outR, curr);

            $.ajax({
                type: 'POST',
                url: saveUrl,
                contentType: "application/json; charset=utf-8",
                dataType: 'json',

                data: $.toJSON({
                    inspectionFormID: 'fbde6eda-dde6-4ba9-b82d-3a35349415f0',

                    result: outR
                }),

                error: function (jqXHR, textStatus, errorThrown) {
                    alert('save failed');
                },

                success: function (data, textStatus, jqXHR) {
                    printObj(data, curr);
                }
            });
        });

        $('button.c').click(function () {
            var curr = $(this).next(),
                outR = {
                    Double: 12.5,
                    DoubleNull: 13.5,
                    Decimal: 14.5,
                    DecimalNull: 15.5,
                    Double2: 16.5,
                    Double2Null: 17.5,
                    Decimal2: 18.5,
                    Decimal2Null: 19.5,
                    Single: 20.5,
                    SingleNull: 21.5,
                    Float: 22.5,
                    FloatNull: 23.5,
                    Int: 24.5,
                    IntNull: 25.5,
                    Int64: 26.5,
                    Int64Null: 27.5
                };

            $('<hr />').appendTo(curr);
            printObj(outR, curr);

            $.ajax({
                type: 'POST',
                url: saveUrl,
                contentType: "application/json; charset=utf-8",
                dataType: 'json',

                data: $.toJSON({
                    'inspectionFormID': 'fbde6eda-dde6-4ba9-b82d-3a35349415f0',

                    'result': outR
                }),

                error: function (jqXHR, textStatus, errorThrown) {
                    alert('save failed');
                },

                success: function (data, textStatus, jqXHR) {
                    printObj(data, curr);
                }
            });
        });

        $('button.d').click(function () {
            var curr = $(this).next(),
                outR = {
                    Double:         '12',
                    DoubleNull:     '13',
                    Decimal:        '14',
                    DecimalNull:    '15',
                    Double2:        '16',
                    Double2Null:    '17',
                    Decimal2:       '18',
                    Decimal2Null:   '19',
                    Single:         '20',
                    SingleNull:     '21',
                    Float:          '22',
                    FloatNull:      '23',
                    Int:            '24',
                    IntNull:        '25',
                    Int64:          '26',
                    Int64Null:      '27'
                };

            $('<hr />').appendTo(curr);
            printObj(outR, curr);

            $.ajax({
                type: 'POST',
                url: saveUrl,
                contentType: "application/json; charset=utf-8",
                dataType: 'json',
                data: $.toJSON({
                    'inspectionFormID': 'fbde6eda-dde6-4ba9-b82d-3a35349415f0',

                    'result': outR
                }),

                error: function (jqXHR, textStatus, errorThrown) {
                    alert('save failed');
                },

                success: function (data, textStatus, jqXHR) {
                    printObj(data, curr);
                }
            });
    });

    $('button.e').click(function () {
        var curr = $(this).next(),
                outR = {
                    Double:         '12.5',
                    DoubleNull:     '13.5',
                    Decimal:        '14.5',
                    DecimalNull:    '15.5',
                    Double2:        '16.5',
                    Double2Null:    '17.5',
                    Decimal2:       '18.5',
                    Decimal2Null:   '19.5',
                    Single:         '20.5',
                    SingleNull:     '21.5',
                    Float:          '22.5',
                    FloatNull:      '23.5',
                    Int:            '24.5',
                    IntNull:        '25.5',
                    Int64:          '26.5',
                    Int64Null:      '27.5'
                };

        $('<hr />').appendTo(curr);
        printObj(outR, curr);

        $.ajax({
            type: 'POST',
            url: saveUrl,
            contentType: "application/json; charset=utf-8",
            dataType: 'json',
            data: $.toJSON({
                'inspectionFormID': 'fbde6eda-dde6-4ba9-b82d-3a35349415f0',

                'result': outR
            }),

            error: function (jqXHR, textStatus, errorThrown) {
                alert('save failed');
            },

            success: function (data, textStatus, jqXHR) {
                printObj(data, curr);
            }
        });
});
    });

</script>

Run it click each button once, and look at the before/after. Thanks in advance for any insight, correction or help that you can provide to resolve this issue.

you can also download the code samples listed above and see the official bug report for this at this link: http://aspnet.codeplex.com/workitem/8114


EDIT: I am including this image to help everyone get what's going on here

click here to see the screenshot of the included example running

Basically: { propertyThatIsADecimal: 54 } becomes { propertyThatIsADecimal: 0 } on the server for multiple different number types in varying scenarios which there doesn't seem to be any rhyme or reason to.

The reason is because when MVC encounters a number it treats it as an Int32 . As such there are no converters, for some reason, from Int32 to say a Decimal or a Nullable<Int64> . There are a couple ways around this issue. Strings, as you already have in your project or to create a custom model binder.

public class JsonTestModelBinder : IModelBinder {
    public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
        JsonTest result = new JsonTest();

        foreach (var property in typeof(JsonTest).GetProperties()) {
            //the value provider starts with the name of the property we're binding to
            //i'm not sure if this changed or not as i don't recall having to do this
            //before - you can remove "result." if your needs don't require it
            var value = bindingContext.ValueProvider.GetValue("result." + property.Name);
            if (value != null && value.RawValue != null) {
                //are we binding to a nullable?
                if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition().Equals(typeof(Nullable<>))) {
                    property.SetValue(result, Convert.ChangeType(value.AttemptedValue, new NullableConverter(property.PropertyType).UnderlyingType), null);
                } else {
                    property.SetValue(result, Convert.ChangeType(value.AttemptedValue, property.PropertyType), null);
                }
            }
        }

        return result;
    }
}

I'm not entirely sure why we still can't convert from Int32 to Decimal but the problem exists within MVC's ValueProviderResult.ConvertSimpleType . It's using TypeDescriptor.GetConverter(your propertyType) and there's no conversions available for those types.

I don't like this particular method but it's the only one available to you for now.

I only glanced at your question, so I apologize if this doesn't apply. But I remember encountering a few issues with Json:

  1. Was this a binding issue? If so, maybe you can implement a custom binding. (Apparently MVC3 uses the JsonFactory already).
  2. I can't remember the issue, but I needed to add JsonRequestBehaviour.AllowGet when making jQuery calls using .getJSON.
  3. Does it matter that you don't have a [HttpGet] or [HttpPost] on your Save method?

I downloaded your code and ran the example. When I clicked the button I received a JavaScript error (Microsoft JScript runtime error: Object doesn't support this property or method) at:

$.ajax({
            type: 'POST',
            url: saveUrl,
            contentType: "application/json; charset=utf-8",
            dataType: 'json',

            data: $.toJSON({
                inspectionFormID: 'fbde6eda-dde6-4ba9-b82d-3a35349415f0',

                result: outR
            }),

            error: function (jqXHR, textStatus, errorThrown) {
                alert('save failed');
            },

            success: function (data, textStatus, jqXHR) {
                printObj(data, curr);
            }
        });

It was also not clear to me from your description what the "before/after" was supposed to be. Can you distill this down to the absolute simplest case with just a single example? I'm assuming that if we can figure it out for that case, then it will apply to the rest.

I'm fighting against this same problem, but unfortunately it's not feasible for me to do any sort of transformation prior to serialization. I still have yet to find a solution.

Also, it's not just the nullable types and decimals that aren't being deserialized for me - a couple of strings are null, and every single property on an array of child entities also show up as null.

I had suspected that it might be an issue with deserialization to an Entity Framework POCO proxy class, but that obviously isn't the problem after seeing your code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM