JavaScriptSerializer 和 ASP.Net MVC 模型绑定产生不同的结果

发布于 2024-12-28 04:03:45 字数 1009 浏览 2 评论 0原文

我看到一个 JSON 反序列化问题,无法解释或修复。

代码

public class Model
{
    public List<ItemModel> items { get; set; }
}
public class ItemModel
{

    public int sid { get; set; }
    public string name { get; set; }
    public DataModel data { get; set; }
    public List<ItemModel> items { get; set; }
}

public class DataModel
{
    public double? d1 { get; set; }
    public double? d2 { get; set; }
    public double? d3 { get; set; }
}

public ActionResult Save(int id, Model model) {
}

数据

{'items':[{'sid':3157,'name':'名字','items':[{'sid':3158,'name':'孩子名字','data ':{'d1':2,'d2':null,'d3':2}}]}]}

单元测试 - 通过

var jss = new JavaScriptSerializer();
var m = jss.Deserialize<Model>(json);
Assert.Equal(2, m.items.First().items.First().data.d1);

相同的 JSON 字符串的问题

,当发送到 Save< /代码> 行动,不会以相同的方式反序列化,特别是 D1、D2 和 D3 值都设置为 NULL。总是。

这是怎么回事?我该如何解决?

I'm seeing a JSON deserialization problem which I can not explain or fix.

Code

public class Model
{
    public List<ItemModel> items { get; set; }
}
public class ItemModel
{

    public int sid { get; set; }
    public string name { get; set; }
    public DataModel data { get; set; }
    public List<ItemModel> items { get; set; }
}

public class DataModel
{
    public double? d1 { get; set; }
    public double? d2 { get; set; }
    public double? d3 { get; set; }
}

public ActionResult Save(int id, Model model) {
}

Data

{'items':[{'sid':3157,'name':'a name','items':[{'sid':3158,'name':'child name','data':{'d1':2,'d2':null,'d3':2}}]}]}

Unit test - passing

var jss = new JavaScriptSerializer();
var m = jss.Deserialize<Model>(json);
Assert.Equal(2, m.items.First().items.First().data.d1);

The problem

the same JSON string, when sent to the Save action, doesn't get deserialized the same way, specially the D1, D2, and D3 values are all set to NULL. Always.

What's going on here, and how can I fix it?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

只有影子陪我不离不弃 2025-01-04 04:03:45

这听起来可能违反直觉,但您应该将这些双精度数作为 json 中的字符串发送:

'data':{'d1':'2','d2':null,'d3':'2'}

这是我的完整测试代码,它使用 AJAX 调用此控制器操作,并允许绑定到模型的每个值:

$.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) {
        // ...
    }
});

并且只是为了说明尝试从 JSON 反序列化数字类型的问题,让我们举几个例子:

  • public double? Foo { 得到;放; }
    • { foo: 2 } => Foo = null
    • { foo: 2.0 } => Foo = null
    • { foo: 2.5 } => Foo = null
    • { foo: '2.5' } =>;富 = 2.5

  • 公共持股量? Foo { 得到;放; }
    • { foo: 2 } => Foo = null
    • { foo: 2.0 } => Foo = null
    • { foo: 2.5 } => Foo = null
    • { foo: '2.5' } =>;富 = 2.5

  • 公共小数? Foo { 得到;放; }
    • { foo: 2 } => Foo = null
    • { foo: 2.0 } => Foo = null
    • { foo: 2.5 } =>富 = 2.5
    • { foo: '2.5' } =>;富 = 2.5

现在让我们对不可为 null 的类型执行相同的操作:

  • public double Foo { get;放; }
    • { foo: 2 } => Foo = 2.0
    • { foo: 2.0 } => Foo = 2.0
    • { foo: 2.5 } =>富 = 2.5
    • { foo: '2.5' } =>;富 = 2.5

  • public float Foo { get;放; }
    • { foo: 2 } => Foo = 2.0
    • { foo: 2.0 } => Foo = 2.0
    • { foo: 2.5 } =>富 = 2.5
    • { foo: '2.5' } =>;富 = 2.5

  • 公共十进制 Foo { get;放; }
    • { foo: 2 } =>富 = 0
    • { foo: 2.0 } =>富 = 0
    • { foo: 2.5 } =>富 = 2.5
    • { foo: '2.5' } =>;富 = 2.5

结论:从 JSON 反序列化数字类型是一团糟。使用 JSON 中的字符串。当然,当您使用字符串时,请注意小数点分隔符,因为它取决于文化。


有人在评论部分问我为什么这可以通过单元测试,但在 ASP.NET MVC 中不起作用。答案很简单:这是因为 ASP.NET MVC 所做的事情远不止单元测试所做的简单调用 JavaScriptSerializer.Deserialize。所以你基本上是在比较苹果和橙子。

让我们更深入地了解发生了什么。在 ASP.NET MVC 3 中,有一个内置的 JsonValueProviderFactory ,它在内部使用 JavaScriptDeserializer 类来反序列化 JSON。正如您已经看到的,这在单元测试中是有效的。但 ASP.NET MVC 中还有更多功能,因为它还使用负责实例化操作参数的默认模型绑定器。

如果您查看 ASP.NET MVC 3 的源代码,更具体地说是 DefaultModelBinder.cs 类,您会注意到为每个要设置值的属性调用以下方法:

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;
        }
    }

    ...............

}

让我们更具体地关注以下行:

object convertedValue = valueProviderResult.ConvertTo(destinationType);

如果我们假设您有一个 Nullable 类型的属性,那么当您调试应用程序时,它会是什么样子:

destinationType = typeof(double?);

这里没有什么意外。我们的目标类型是 double? 因为这是我们在视图模型中使用的类型。

然后看一下 valueProviderResult

在此处输入图像描述

查看此 RawValue< /code> 财产在那里?你能猜出它的类型吗?

在此处输入图像描述

所以这个方法只是抛出异常,因为它显然无法转换 decimal2.5double?

您注意到在这种情况下返回了什么值吗?这就是为什么您的模型最终会出现 null 的原因。

这很容易验证。只需检查控制器操作中的 ModelState.IsValid 属性,您就会发现它是 false。当您检查添加到模型状态的模型错误时,您将看到以下内容:

参数从“System.Decimal”类型到类型的转换
'System.Nullable`1[[System.Double,mscorlib,版本=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089]]' 失败,因为没有
类型转换器可以在这些类型之间进行转换。

您现在可能会问,“但是为什么 ValueProviderResult 中的 RawValue 属性是十进制类型?”。答案再次隐藏在 ASP.NET MVC 3 源代码中(是的,您现在应该已经下载了它)。让我们看一下 JsonValueProviderFactory.cs 文件,更具体地说是 GetDeserializedObject 方法:

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;
    }

    ............

}

您是否注意到以下行:

JavaScriptSerializer serializer = new JavaScriptSerializer();
object jsonData = serializer.DeserializeObject(bodyText);

您能猜出以下代码片段将在您的计算机上打印什么吗?安慰?

var serializer = new JavaScriptSerializer();
var jsonData = (IDictionary<string, object>)serializer
    .DeserializeObject("{\"foo\":2.5}");
Console.WriteLine(jsonData["foo"].GetType());

是的,您猜对了,它是一个十进制

您现在可能会问,“但是为什么他们在我的单元测试中使用serializer.DeserializeObject方法而不是serializer.Deserialize?”这是因为 ASP.NET MVC 团队做出了使用 ValueProviderFactory 实现 JSON 请求绑定的设计决策,而它不知道模型的类型。

现在看看您的单元测试与 ASP.NET MVC 3 背后实际发生的情况有何完全不同?通常应该解释为什么它会通过,以及为什么控制器操作没有获得正确的模型值?

It might sound counter-intuitive, but you should send those doubles as strings in the json:

'data':{'d1':'2','d2':null,'d3':'2'}

Here is my complete test code that invokes this controller action using AJAX, and allows binding to every value of the model:

$.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 trying to deserialize numeric types from JSON, let's take 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 the JSON. And of course, when you use strings, be careful with the decimal separator as it is culture dependent.


I have been asked in the comments section why this passes unit tests, but doesn't work in ASP.NET MVC. The answer is simple: It's because ASP.NET MVC does many more things than a simple call to a JavaScriptSerializer.Deserialize, which is what the unit test does. So you are basically comparing apples to oranges.

Let's dive deeper into what happens. In ASP.NET MVC 3 there's a built-in JsonValueProviderFactory which internally uses the JavaScriptDeserializer class to deserialize the JSON. This works, as you have already seen, in the unit test. But there's much more to it in ASP.NET MVC, as it also uses a default model binder that is responsible for instantiating your action parameters.

And if you look at the source code of ASP.NET MVC 3, and more specifically the DefaultModelBinder.cs class, you will notice the following method which is invoked for each property that will have a value 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;
        }
    }

    ...............

}

Let's focus more specifically on the following line:

object convertedValue = valueProviderResult.ConvertTo(destinationType);

If we suppose that you had a property of type Nullable<double>, here's what this would look like when you debug your application:

destinationType = typeof(double?);

No surprises here. Our destination type is double? because that's what we used in our view model.

Then take a look at the valueProviderResult:

enter image description here

See this RawValue property out there? Can you guess its type?

enter image description here

So this method simply throws an exception because it obviously cannot convert the decimal value of 2.5 to a double?.

Do you notice what value is returned in this case? That's why you end up with null in your model.

That's very easy to verify. Simply inspect the ModelState.IsValid property inside your controller action and you will notice that it is false. And when you inspect the model error that was added to the model state you will see this:

The parameter conversion from type 'System.Decimal' to 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.

You may now ask, "But why is the RawValue property inside the ValueProviderResult of type decimal?". Once again the answer lies inside the ASP.NET MVC 3 source code (yeah, you should have downloaded it by now). Let's take a 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;
    }

    ............

}

Do you notice the following line:

JavaScriptSerializer serializer = new JavaScriptSerializer();
object jsonData = serializer.DeserializeObject(bodyText);

Can you guess what the following 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());

Yep, you guessed it right, it's a decimal.

You may now ask, "But why did they use the serializer.DeserializeObject method instead of serializer.Deserialize as in my unit test?" It's because the ASP.NET MVC team made the design decision to implement JSON request binding using a ValueProviderFactory, which doesn't know the type of your model.

See now how your unit test is completely different than what really happens under the covers of ASP.NET MVC 3? Which normally should explain why it passes, and why the controller action doesn't get a correct model value?

生活了然无味 2025-01-04 04:03:45

解决方案1:传递标记为“application/x-www-form-urlencoded”的数据。在这种情况下,Nullable 被正确反序列化。
例子:

<script type="text/javascript">

$("#post-data").click(function () {
    $.ajax({
        url: "/info/create",
        type: "PUT",
        //  contentType: 'application/json', //default is application/x-www-form-urlencoded
        data: JSON.stringify({
            Dbl1: null, //pass double as null
            Dbl2: 56.3  //pass double with value
        }),
        dataType: "json"
    });

    return false;
});

解决方案2:改变double?到十进制?并将内容发送为“application/json”。感谢达林调查

Solution 1: Pass data marked as "application/x-www-form-urlencoded". In this case Nullable<double> is deserialized correctly.
Example:

<script type="text/javascript">

$("#post-data").click(function () {
    $.ajax({
        url: "/info/create",
        type: "PUT",
        //  contentType: 'application/json', //default is application/x-www-form-urlencoded
        data: JSON.stringify({
            Dbl1: null, //pass double as null
            Dbl2: 56.3  //pass double with value
        }),
        dataType: "json"
    });

    return false;
});

Solution 2: change double? to decimal? and send content as "application/json". Thanks to Darin investigation

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文