Serialización JSON en Tokyo « Puro Delphi

Cursos, artículos, noticias y herramientas Delphi VCL (Visual Component Library) / FMX (Firemonkey)

Serialización JSON en Tokyo

Entre las novedades de Tokyo que no se han documentado hasta el momento, podemos encontrar dos nuevas unidades en la RTL de Delphi: System.JSON.Serializers System.JSON.Converters.

Se trata de unidades que sirven para trabajar con JSON, en particular, convertir objetos Delphi a JSON, y también el proceso inverso, crear un objeto Delphi a partir de una estructura JSON.

Como se trata de algo que aún no hay documentación oficial, lo que he podido investigar ha sido a prueba y error, usando Delphi Tokyo Starter (por lo que no tengo acceso al código fuente) y sacando mis propias conclusiones. Aún así, la biblioteca es bastante sencilla de usar y muy directa al grano.

De las dos unidades la más importante es System.JSON.Serializers, en donde se define la clase TJsonSerializer que es la que utilizaremos para trabjar. Esta clase es la encargada de pasar un objeto o record Delphi a su equivalente en JSON (serialización), y también puede crear un objeto o record leyendo o interpretando una estructura JSON (deserialización).

Veamos un ejemplo sencillo.

Primero he declarado una clase TPerson en la cual puse entre sus atributos datos de distintos tipos para probar la serialización con los distintos tipos de datos que maneja Delphi:

Y la implementación consiste en un constructor que inicializa la instancia con todos los parámetros que recibe, almacenándolos en las variables de instancia que corresponde:

Hasta acá nada espectacular. Veamos ahora como podemos obtener el objeto JSON equivalente usando TJsonSerializer:

Para eso agregamos un método publico ToJsonString y lo implementamos así:

Podemos probar rápidamente ahora este código. Creamos una aplicación Vcl o Firemonkey, ponemos un TButton y un TMemo y el siguiente código en el evento OnClick del botón:

Si hacemos click en el botón, en el memo deberíamos ver lo siguiente:

Fíjense que el JSON obtenido es el correcto de acuerdo a como yo lo he creado en el código de ejemplo, pero resulta incomodo de leer. Lo más común cuando vemos objetos JSON en internet es en su formato con sangría, que para nuestro caso sería así:

Existe una manera muy sencilla de generar esto y es simplemente indicándole a la clase TJsonSerializer que utilice el formato con sangría, de este modo:

Existen algunas propiedades más que permiten cambiar cómo se crea el JSON resultante; la mayoría, como TJsonFormatting están definidas en la unidad System.JSON.Types:

PropiedadTipoEfecto
DateFormatHandlingTJsonDateFormatHandlingEspecifica el formato para tipos TDate, TDateTime, TTime

Nota: Esta propiedad solo tiene efecto cuando serializamos JSON

DateParseHandlingTJsonDateParseHandlingDetermina como se deben interpretar las fechas al leer un objeto JSON

 

Nota: Esta propiedad sólo tiene efecto cuando deserializamos JSON

DateTimeZoneHandlingTJsonDateTimeZoneHandlingDetermina si las fechas deben utilizar la configuración de zona horaria local o UTC
FloatFormatHandlingTJsonFloatFormatHandlingDefine el formato de salida de los números especiales para coma flotante como “NaN”, “Infinity”, “-Infinity”
FormattingTJsonFormattingEspecifica el formato con sangría o en línea

Nota: Esta propiedad sólo tiene efecto cuando serializamos JSON

MaxDepthIntegerEl máximo “nivel de profundidad” a leer cuando deserializamos JSON. Con el nivel de profundidad hablamos de los objetos contenidos dentro del objeto que estamos deserializando. Podría ser un caso que nuestra clase tenga objetos contenidos dentra de ella que no forman parte de la estructura del JSON que estamos parseando, esta propiedad nos permite controlar cuantos niveles de anidación queremos procesar

Nota: Esta propiedad sólo tiene efecto cuando deserializamos JSON

ObjectHandlingTJsonObjectHandlingEstá relacionada con el atributo JsonObjectHandling. Es el valor por defecto del atributo cuando no fue especificado
ObjectOwnershipTJsonObjectOwnershipEstá relacionada con el atributo JsonObjectOwnership. Es el valor por defecto del atributo cuando no fue especificado
StringEscapeHandlingTJsonStringEscapeHandlingDetermina la secuencia de escape al procesar strings

Nota: Esta propiedad sólo tiene efecto cuando serializamos JSON

Nota: El efecto de las propiedades fue una conclusión luego de realizar unas simples pruebas. En algunos casos los tipos de las propiedades definidos en System.JSON.Types tienen documentación XML embebida (la que sale en el mismo editor de código) y explican su propósito, aunque pueda haber algún error, ya que por ejemplo TJsonEmptyValueHandlingTJsonDateFormatHandling rezan lo mismo:

Specifies the handling for empty values in managed types like array and string([], ”)

Existe otra cuestión que quizá nos interese modificar. Si volvemos a mirar el JSON que generamos mas arriba, veremos que en realidad lo que proceso son las variables de instancia (FName, FAge, etc). Cuando en realidad el JSON que podría querer generar debería ser: Name, Age, etc

Aquí es donde entran en juego una serie de atributos definidos en la unidad System.JSON.Serializers

NombreAplicable aEfecto
JsonConverterAttributeTipos

Variables de Instancia

Propiedades

Permite especificar un conversor (Converter) que se encargará de la serialización, en lugar de usar el conversor por defecto
JsonIgnoreAttributeTipos

Variables de Instancia

Propiedades

Indica al framework que debe ignorar este valor, es decir, no forma parte del objeto JSON pero por diferentes motivos nuestro objeto Delphi necesita de este miembro
JsonNameAttributeVariables de Instancia

Propiedades

Es la manera de indicar un alias para el miembro
JsonInAttributeVariables de Instancia

Propiedades

Este atributo se utiliza en combinación con el atributo  JsonSerializeAttribute y se le da el valor In

Indica que el miembro anotado forma parte del JSON 

JsonSerializeAttributeTiposEl tipo de serialización, podemos indicar que queremos procesar:

sólo los miembros privados (JsonMemberSerialization.Fields),

sólo los miembros públicos,

(JsonMemberSerialization.Public),

sólo los miembros que esten anotados con el atributo JsonInAttribute (ver más arriba)

(JsonMemberSerialization.In)

Siguiendo con nuestra idea, para solucionar el problema del prefijo “F” que tienen los elementos de mi JSON, puedo hacerlo de dos maneras:

Puedo utilizar el atributo JsonSerialize y anotar a la clase TPerson, indicando que solo quiero que procese los miembros públicos de la clase

Resultado:

Fijense que no aparecen las variables de instancia FMail y FGenders, ya que no estaban en la parte publica de la clase. También el orden de los elementos cambia porque las propiedades tienen un orden distinto al de las variables de instancia (ver definicion de la clase TPerson)

O también podría haber utilizado el atributo JsonName e ir anotando los miembros que me interesen, así:

Y en este caso obtenemos:

Otra cosa que tiene medio fea el JSON que generamos es que los tipos enumerativos y los conjuntos están representados usando enteros. Si quisiera tener una representación de mayor nivel, es decir, mas expresiva, por ejemplo con los nombres de los tipos, debemos utilizar Conversores

Aquí es donde entra en juego la unidad System.JSON.Converters. En esta unidad, se implementan algunas subclases de System.JSON.Serializers.TJsonConverter cuyo trabajo es procesar de una manera especial determinados tipos de datos. Por ejemplo, si quiero que el campo “FGender” o “FPersonType” del JSON anterior salga el valor en string de acuerdo a como fue definido el tipo enumerado (es decir, TGender.Male, TPersonType.ptVip, etc) debemos hacer uso del atributo JsonConverter. Este atributo recibe como parámetro una clase que es la que debe utilizar el framework cuando convierte este tipo de datos:

Ahora nuestro JSON se verá así:

Y para el conjunto “FGenders” tenemos el conversor TJsonSetNamesConverter

Por lo que nuestro JSON ahora se ve de esta manera:

Hay algunos conversores más definidos, que trabajan con colecciones, por ejemplo, listas, diccionarios, etc. Veamos un ejemplo con listas, para este caso voy a agregar un nuevo botón con este código:

Y al ejecutarlo, veremos este objeto JSON en el memo:

Podemos ver que en este caso, Delphi ha incluido también cosas que quizá no nos esperábamos, que son atributos del objeto TObjectList; probablemente no nos interese que esto sea parte de nuestro JSON, y por fortuna, existe un mecanismo bastante fácil para deshacernos de ello. En este caso vamos a utilizar los atributos JsonSerialize y JsonIn

Recordemos que cuando anotamos un tipo con el atributo JsonSerialize en su valor TJsonMemberSerialization.In, podemos controlar exactamente que miembros queremos que el framework procese, ya que sólo serán serializados aquellos que sean anotados con el atributo JsonIn.

Para poder hacer esto, primero tenemos que definir una subclase de TObjectList<T>, y luego la anotamos con el atributo, así:

Y ahora nuestro JSON solo muestra lo que nos importa, que es la lista de usuarios:

Conclusión: Creo que con un estándar tan asentado como lo es JSON hoy en día, es de esperarse que entre las bibliotecas que se distribuyen con un lenguaje exista algo que permita trabajar con este formato de una manera sencilla: prácticamente no hemos escrito más código que el de nuestra aplicación (a pesar de que sea un ejemplo de lo más sencillo), el framework está “a un costado”, haciendo elegantemente su trabajo, sin que nosotros tengamos que intervenir mucho más que para configurar algunas cuestiones, pero se puede hacer de una manera muy sencilla, moderna y elegante como es el uso de atributos, además sin necesidad de escribir código que ensucie nuestra aplicación. Y en el caso de existir casos más complejos, el framework es extensible permitiéndonos crear nuestros propios conversores y luego indicarle que los use.

Aunque mi impresión final es muy buena, no podía dejar de comentar que hay un defecto que puede ser no menor en alguna aplicación que utilice intensivamente el framework (pensemos en un WebService que devuelva objetos JSON utilizando TJsonSerializer) y es que hay fugas de memoria cuando se utiliza el atributo JsonConverter. El framework parece no liberar la memoria del conversor que le indicamos que utilice. Y puede ser bastante grave porque por cada serialización, tendríamos una fuga de memoria.

Si colocamos la bandera ReportMemoryLeaksOnShutdown podemos comprobarlo fácilmente:

Si luego hacemos varias veces click en los botones que veníamos usando, y luego cerramos la aplicación, obtendríamos un mensaje similar a este:

—————————
Unexpected Memory Leak
—————————
An unexpected memory leak has occurred. The unexpected small block leaks are:

1 – 12 bytes: TJsonSetNamesConverter x 9, TJsonEnumNameConverter x 9

En esta entrada solamente hemos visto la parte de la serialización; y ya se ha hecho lo bastante extensa, por lo que en una entrada futura veremos la otra parte que nos queda, la deserialización.

Recursos adicionales: No puedo dejar de mencionar estos dos enlaces que me han ayudado a entender estas unidades, están en idioma Japonés, por lo que podría ser necesario utilizar algún traductor para poder leerlas. En ellas hay más ejemplos, por lo que recomiendo su lectura y estudio:

How to use TJsonSerializer

Practical example of TJsonSerializer


11 Comentarios

  • Simplemente genial Agustín, muchas gracias por compartirlo con nosotros, he aprendido mucho de esta entrada y tu muy valiosa investigación.

    Me ha parecido mucho más atractiva esta nueva opción que la que ya venía de fabrica llamada TJSONMarshal, puesto que da verdadera libertad e integración con el estándar.

    • Ya lo he votado, espero que tenga muchos votos para que sea solucionado en la próxima actualización o ¿por qué no?, en algún fixed lanzado previamente… yo lo veo como algo indispensable si se quiere implementar un verdadero Servicio REST Full 🙂

  • Gracias Augustin por su puesto.
    Pero no queriendo molestar, ¿podría usted ayudarme?
    En mi cobertura una propiedad del tipo TDate se está quedando con un decimal (por ejemplo: 24329), y me gustaría que se quedara por ejemplo: 2015-01-31 y no estoy sabiendo cómo hacerlo. Me quedaría muy agradecido si me puede ayudar. Gracias.

    • Hola Jeferson,

      El estandar JSON en si mismo no especifica nada en cuanto a las fechas y cual es su formato esperado. Pero JavaScript si, y en JavaScript la clase Date, al invocar a su metodo toJSON el formato emitido corresponde a la fecha+hora+zona horaria que es lo que especifica el estandar ISO 8601.

      En Delphi el tipo TDate carece de la hora es por eso que el serializer decidio que lo mejor es tratar la fecha como valores flotantes.

      Yo te diria que cambies tus tipos a variables de tipo TDateTime. Si esto no es posible, podrias marcar con el atributo JsonIgnore a la variable TDate, y crear una nueva variable (o funcion) TDateTime

      • Hola amigo augustin,

        Las dos formas que usted mencionó funcionan bien para mí, podemos adaptar a un estándar en Delphi siempre trabajando con TDateTime o cada caso será un caso.
        Agradezco su gentileza y rápida respuesta a mi pregunta.
        De nuevo muchas gracias.

Escriba un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

  • RSS

  • Categorías

  • Nube de etiquetas

  • Usuario