Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 5 de abril de 2022
.NET

Al hilo del post Cómo recibir un JSON como string en una acción ASP.NET Core MVC, el amigo Alberto dejaba una interesante pregunta en los comentarios: ¿y si una vez hemos recibido el string, queremos validar que sea un JSON válido?

Obviamente, una forma sencilla sería intentar deserializarlo por completo a la clase de destino, siempre que ésta sea conocida. Para ello podríamos utilizar el método Deserialize<T>() del objeto JsonSerializer de System.Text.Json, disponible en todas las versiones modernas de .NET, de la siguiente manera:

public bool IsJson<T>(string maybeJson)
{
    try {
        var obj = JsonSerializer.Deserialize<T>(maybeJson);
        return true;
    } catch 
    { 
        return false; 
    }
}

Si no tenemos o conocemos la clase específica a la que habría que deserializar, podríamos hacerlo de forma genérica sobre un object:

public bool IsJson(string maybeJson)
{
    try {
        var obj = JsonSerializer.Deserialize<object>(maybeJson);
        return true;
    } catch 
    { 
        return false; 
    }
}

Pero claro, si en realidad no nos interesa hacer nada con los valores del JSON, sino simplemente queremos saber si es JSON válido o no, con estas opciones anteriores estaríamos matando moscas a cañonazos. Quizás sería una mejor opción utilizar JsonDocument.Parse(), que creará la estructura en memoria con el contenido del JSON, pero no intentará materializarlo sobre ningún objeto del CLR:

public bool IsJsonDocument(string maybeJson)
{
    try
    {
        var obj = JsonDocument.Parse(maybeJson);
        return true;
    }
    catch
    {
        return false;
    }
}

Mmmm... y entonces, ¿cuál es la mejor forma?

Cualquiera de las opciones anteriores son eficientes y muy rápidas, por lo que la mayoría de las veces podremos elegir la que mejor nos venga. Pero si queremos afinar algo más, como ocurre casi siempre, la mejor opción depende de lo que vayamos buscando ;)

Para comprobarlo, vamos a recurrir a nuestro viejo amigo BenchmarkDotNet. Creamos una aplicación de consola, con una referencia al paquete NuGet BenchMarkDotNet.

Añadimos en la carpeta del proyecto un archivo JSON de prueba (sample.json) y creamos también en él un objeto .NET con su estructura (esto podemos generarlo fácilmente mediante la opción de Visual Studio Edit > Paste Special> Paste JSON as classes).

Creamos también una clase con las pruebas a realizar:

[MemoryDiagnoser]
public class JsonCheckBenchmarks
{
    public string _maybeJson;

    [GlobalSetup]
    public void Setup()
    {
        var filePath = Path.Combine(Assembly.GetCallingAssembly().Location, "..", "sample.json");
        _maybeJson = File.ReadAllText(filePath);
    }

    [Benchmark]
    public void JsonDocumentParse()
    {
        IsJsonDocument(_maybeJson);
    }

    [Benchmark]
    public void JsonSerializerDeserializeGeneric()
    {
        IsJson<RootObject>(_maybeJson);
    }

    [Benchmark]
    public void JsonSerializerDeserializeObject()
    {
        IsJson(_maybeJson);
    }

    ... // Los métodos IsJson(), IsJson<T>() y IsJsonDocument() que hemos visto antes
}

Y finalmente, lanzamos las pruebas desde el archivo Program.cs:

BenchmarkRunner.Run<JsonCheckBenchmarks>(); // Go!

Al ejecutar el proyecto (¡recordad, siempre en modo Release!) se ejecutarán las pruebas para comparar las tres fórmulas utilizadas para chequear la validez del JSON. Tras esperar unos minutos, el resultado que obtenemos es el siguiente cuando el contenido del archivo JSON es correcto:

|                           Method |     Mean |     Error |    StdDev |  Gen 0 | Allocated |
|--------------------------------- |---------:|----------:|----------:|-------:|----------:|
|                JsonDocumentParse | 1.923 us | 0.0109 us | 0.0096 us | 0.2594 |      2 KB |
| JsonSerializerDeserializeGeneric | 2.686 us | 0.0071 us | 0.0059 us | 0.1411 |      1 KB |
|  JsonSerializerDeserializeObject | 3.279 us | 0.0115 us | 0.0096 us | 0.1602 |      1 KB |

Como podemos observar, cuando el JSON es correcto, la opción más rápida es usar JsonDocument.Parse(). Tiene sentido, puesto que nos ahorramos todo el trabajo de crear un nuevo objeto y cargar sus propiedades vía reflexión. Y de las opciones que usan deserialización, la que utiliza el tipo concreto es bastante más rápida que la que deserializa a object.

Sin embargo, se puede observar también que las opciones que deserializan el objeto desde JSON, aunque menos rápidas, son más eficientes en términos de memoria. También tiene sentido, pues no tienen crear la estructura en memoria para alojar el JSON, sino crear los objetos destino de la deserialización y sus propiedades.

Por tanto, si pensamos que la mayoría de los JSON que nos lleguen serán correctos, deberíamos elegir JsonDocument.Parse() si queremos optimizar en velocidad, o la deserialización genérica JsonSerializer.Deserialize<T>() si nos interesa optimizar la memoria.

En cambio, al introducir algún problema en el archivo JSON (por ejemplo, eliminar la llave de cierre), los datos cambian un poco:

|                           Method |     Mean |    Error |   StdDev |  Gen 0 | Allocated |
|--------------------------------- |---------:|---------:|---------:|-------:|----------:|
|                JsonDocumentParse | 24.86 us | 0.043 us | 0.036 us | 0.1831 |      2 KB |
| JsonSerializerDeserializeGeneric | 26.20 us | 0.509 us | 0.476 us | 0.5798 |      5 KB |
|  JsonSerializerDeserializeObject | 29.70 us | 0.116 us | 0.096 us | 0.4272 |      4 KB |

JsonDocument.Parse() sigue siendo la opción más rápida, aunque con muy poca diferencia sobre las opciones de deserialización completa, pero también es el que menos memoria consume. Por tanto, si la mayoría de JSON serán incorrectos, la primera opción será la más razonable.

Actualizado el 19-abr-2022: puedes ver una fórmula aún más eficiente en este otro post: https://www.variablenotfound.com/2022/04/una-forma-mas-eficiente-de-comprobar-si.html

Publicado en Variable not found.

4 Comentarios:

Alberto Baigorria dijo...

Muchas gracias Jose Maria. :)

Quedo muy agradecido por su respuesta! Realizaré las pruebas y les comparto como me fué en mi esceneraio particular.. Además agradecerte por la herramienta para
comprobacion de codigo BenchMarkDotNet, no tenia conocimiento de ella.

Saludos desde Santa Cruz, Bolivia.

José María Aguilar dijo...

Muchas gracias, Alberto.

Pero el tema no ha acabado aquí ;) Escribiré algo sobre este twit en breve: https://twitter.com/javiercampos/status/1511242575507820550

Saludos!

Anónimo dijo...

Al usar JsonSerializer.Deserialize con el object

Me pone cannot convert to string to Newtonsoft.Json.JsonReader

José María Aguilar dijo...

Hola, @anonimo,

este post utiliza la biblioteca System.Text.Json y no Newtonsoft.Json, por eso los ejemplos no te funcionarán. System.Text.Json es la solución proporcionada por el framework para el tratamiento de JSON, y es la que deberías utilizar si quieres ganar en eficiencia y rendimiento (salvo que tu proyecto esté anclado en alguna versión de .NET que no la soporte).

Saludos!