mvc - net core post json
JavaScriptSerializer y ASP.Net MVC modelo vinculante producen resultados diferentes (2)
Estoy viendo un problema de deserialización JSON que no puedo explicar ni corregir.
Código
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) {
}
Datos
{''items'':[{''sid'':3157,''name'':''a name'',''items'':[{''sid'':3158,''name'':''child name'',''data'':{''d1'':2,''d2'':null,''d3'':2}}]}]}
Prueba de unidad - pasando
var jss = new JavaScriptSerializer();
var m = jss.Deserialize<Model>(json);
Assert.Equal(2, m.items.First().items.First().data.d1);
El problema
la misma cadena JSON, cuando se envía a la acción Save
, no se deserializa de la misma manera, especialmente los valores D1, D2 y D3 están todos en NULL. Siempre.
¿Qué está pasando aquí y cómo puedo solucionarlo?
Solución 1: Pase los datos marcados como "application / x-www-form-urlencoded". En este caso Nullable<double>
se deserializa correctamente. Ejemplo:
<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;
});
Solución 2: ¿cambiar doble? ¿a decimal? y enviar contenido como "aplicación / json". Gracias a la investigación de Darin
Puede parecer contra-intuitivo, pero deberías enviar esos dobles como cadenas en json:
''data'':{''d1'':''2'',''d2'':null,''d3'':''2''}
Aquí está mi código de prueba completo que invoca esta acción del controlador usando AJAX, y permite vincular a cada valor del modelo:
$.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) {
// ...
}
});
Y solo para ilustrar el alcance del problema de intentar deserializar los tipos numéricos de JSON, veamos algunos ejemplos:
-
public double? Foo { get; set; }
-
{ foo: 2 }
=> Foo = nulo -
{ foo: 2.0 }
=> Foo = nulo -
{ foo: 2.5 }
=> Foo = nulo -
{ foo: ''2.5'' }
=> Foo = 2.5
-
-
public float? Foo { get; set; }
-
{ foo: 2 }
=> Foo = nulo -
{ foo: 2.0 }
=> Foo = nulo -
{ foo: 2.5 }
=> Foo = nulo -
{ foo: ''2.5'' }
=> Foo = 2.5
-
-
public decimal? Foo { get; set; }
-
{ foo: 2 }
=> Foo = nulo -
{ foo: 2.0 }
=> Foo = nulo -
{ foo: 2.5 }
=> Foo = 2.5 -
{ foo: ''2.5'' }
=> Foo = 2.5
-
Ahora hagamos lo mismo con los tipos que no aceptan nulos:
-
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
-
Conclusión: la deserialización de los tipos numéricos de JSON es un gran desastre. Use cadenas en el JSON. Y, por supuesto, cuando usa cadenas, tenga cuidado con el separador decimal ya que depende de la cultura.
Me han preguntado en la sección de comentarios por qué esto pasa las pruebas unitarias, pero no funciona en ASP.NET MVC. La respuesta es simple: es porque ASP.NET MVC hace muchas más cosas que una simple llamada a un JavaScriptSerializer.Deserialize
, que es lo que hace la prueba unitaria. Entonces básicamente estás comparando manzanas con naranjas.
Vamos a profundizar en lo que sucede. En ASP.NET MVC 3 hay una JsonValueProviderFactory
que usa internamente la clase JavaScriptDeserializer
para deserializar el JSON. Esto funciona, como ya has visto, en la prueba unitaria. Pero hay mucho más en ASP.NET MVC, ya que también utiliza una carpeta de modelo predeterminada que es responsable de crear instancias de sus parámetros de acción.
Y si mira el código fuente de ASP.NET MVC 3, y más específicamente la clase DefaultModelBinder.cs, notará el siguiente método que se invoca para cada propiedad que tendrá un valor que establecer:
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;
}
}
...............
}
Centrémonos más específicamente en la siguiente línea:
object convertedValue = valueProviderResult.ConvertTo(destinationType);
Si suponemos que tenía una propiedad de tipo Nullable<double>
, esto es lo que se vería cuando depure su aplicación:
destinationType = typeof(double?);
No hay sorpresas aquí. Nuestro tipo de destino es double?
porque eso es lo que usamos en nuestro modelo de vista.
Luego eche un vistazo al valueProviderResult
:
Ver esta propiedad RawValue
por ahí? ¿Puedes adivinar su tipo?
Entonces, ¿este método simplemente arroja una excepción porque obviamente no puede convertir el valor decimal
de 2.5
en un double?
.
¿Notan qué valor se devuelve en este caso? Es por eso que terminas con null
en tu modelo.
Eso es muy fácil de verificar. Simplemente inspeccione la propiedad ModelState.IsValid
dentro de su acción de controlador y notará que es false
. Y cuando inspeccione el error del modelo que se agregó al estado del modelo, verá esto:
La conversión de parámetros de tipo ''System.Decimal'' a ''System.Nullable`1 [[System.Double, mscorlib, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089]]'' falló porque ningún convertidor de tipo puede convertir entre estos tipos
Ahora puede preguntar: "¿Pero por qué la propiedad RawValue está dentro de ValueProviderResult de tipo decimal?". Una vez más, la respuesta se encuentra dentro del código fuente de ASP.NET MVC 3 (sí, ya debería haberlo descargado). Echemos un vistazo al archivo JsonValueProviderFactory.cs
y, más específicamente, al método 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;
}
............
}
¿Notan la siguiente línea?
JavaScriptSerializer serializer = new JavaScriptSerializer();
object jsonData = serializer.DeserializeObject(bodyText);
¿Puedes adivinar qué se imprimirá en la consola en el siguiente fragmento?
var serializer = new JavaScriptSerializer();
var jsonData = (IDictionary<string, object>)serializer
.DeserializeObject("{/"foo/":2.5}");
Console.WriteLine(jsonData["foo"].GetType());
Sí, lo adivinaste bien, es un decimal
.
Ahora puede preguntar: "¿Pero por qué utilizaron el método serializer.DeserializeObject en lugar de serializer.Deserialize como en mi prueba unitaria?" Se debe a que el equipo ASP.NET MVC tomó la decisión de diseño de implementar el enlace de solicitud JSON utilizando un ValueProviderFactory
, que no conoce el tipo de su modelo.
Vea ahora cómo su prueba de unidad es completamente diferente de lo que realmente sucede bajo las cubiertas de ASP.NET MVC 3? ¿Qué normalmente debería explicar por qué pasa, y por qué la acción del controlador no obtiene un valor de modelo correcto?