JSON y serialización

Es difícil imaginar una aplicación móvil que no necesite comunicarse con un web server o que no tenga que almacenar fácilmente datos estructurados en algún momento. Cuando creamos apps conectadas a la red, lo más probable es que tengas que consumir algún JSON, tarde o temprano.

Esta guía va sobre las maneras de usar JSON con Flutter. Cubre que solución JSON usar en diferentes escenarios, y porqué.

¿Cual método de serialización JSON es el adecuado para mí?

Este artículo cubre dos estrategias generales para trabajar con JSON:

  • Serialización manual
  • Serialización automática usando auto-generación de código

Diferentes proyectos tienen diferente complejidad y casos de uso. Para proyectos pequeños de prueba de concepto o prototipos rápidos, usar auto-generación de código puede ser exagerado. Para apps con varios modelos JSON con más complejidad, codificar a mano puede volverse rápidamente tedioso, repetitivo, y se presta a muchos pequeños errores.

Usar serialización manual para pequeños proyectos

La decodificación manual de JSON se refiere a usar el decodificador JSON incluido en dart:convert. Esto implica pasar una cadena JSON en bruto a la función json.decode() , y luego buscar los valores que necesita en el Map<String, dynamic> que devuelve el método. Esto no tiene dependencias externas ni un proceso de configuración, y esto es bueno para una prueba de concepto rápida.

La decodificación manual no funciona bien cuando su proyecto se hace más grande. Escribir lógica de decodificación a mano puede convertirse en difícil de manejar y ser propenso a errores. Si tienes un error tipográfico accediendo a un campo inexistente del JSON, tu código lanzará un error en tiempo de ejecución.

Si no tienes muchos modelos JSON en tu proyecto y estás buscando probar un concepto rápidamente, la serialización manual puede ser el camino por él que empezar. Para un ejemplo de codificación manual, mira Serialización manual de JSON usando dart:convert.

Usar auto-generación de código para proyectos medianos o grandes

La serialización JSON con auto-generación de código significa tener una biblioteca externa que genera el boilerplate de codificación para tí. Después de alguna configuración inicial, ejecutas un file watcher que genera el código para las clases de tu modelo. Por ejemplo, json_serializable y built_value son de este tipo de bibliotecas.

Esta aproximación escala mejor para un proyecto grande. Ningún boilerplate escrito a mano es necesario, y los errores tipográficos cuando se accede a los campos del JSON son capturados en tiempo de compilación. La desventaja con la auto-generación de código es que esta necesita alguna configuración inicial. También, el fichero fuente generado puede producir desorden visual en tu navegador del proyecto.

Es posible que desees usar auto-generación de código para serializar JSON cuando tengas proyectos medianos o grandes. Para ver un ejemplo de auto-generación de código basada en codificación de JSON, mira Serializar un JSON usando bibliotecas de auto-generación.

Hay algo equivalente a GSON/Jackson/Moshi en Flutter?

La respuesta simple es no.

Estas librerías necesitarían usar reflexión en tiempo de ejecución, que está desactivada en Flutter. La reflexión en tiempo de ejecución interfiere con tree shaking, que es soportado por Dart desde hace bastante tiempo. Con tree shaking, puedes hacer “shake off” del código no utilizado de tus release builds. Esto optimiza significativamente el tamaño de la app.

Como la reflexión hace que todo el código se usado de forma implícita por defecto, esto hace el tree shaking difícil. Las herramientas no pueden conocer que partes del código no son usadas en tiempo de ejecución, entonces es difícil eliminar el código redundante. El tamaño de la app no puede ser fácilmente optimizado cuando se usa reflexión.

Aunque no puedes usar reflexión en tiempo de ejecución con Flutter, algunas bibliotecas te dan APIs similares, fáciles de usar, pero están basadas en auto-generación de código en su lugar. Esta aproximación es cubierta con más detalle en la sección bibliotecas de auto-generación de código.

Serialización manual de JSON usando dart:convert

La codificación básica de un JSON es muy simple en Flutter. Flutter tiene la biblioteca dart:convert que incluye un sencillo codificador y decodificador JSON.

Aquí hay un ejemplo de un JSON para un modelo sencillo.

{
  "name": "John Smith",
  "email": "john@example.com"
}

Con dart:convert, puedes codificar este modelo JSON de dos maneras.

Serializar un JSON en línea

Mirando en la documentación JSON de dart:convert, verás que puedes decodificar el JSON llamando al método json.decode, con la cadena JSON como argumento del método.

Map<String, dynamic> user = jsonDecode(jsonString);

print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');

Desafortunadamente, jsonDecode() simplemente devuelve un Map<String, dynamic>, significando que no conoces los tipos de valores hasta el tiempo de ejecución. Con esta aproximación, pierdes la mayoría de las características del tipado estático del lenguaje: seguridad de tipos, autocompletado y mucho más importante, las excepciones en tiempo de compilación. Tu código será más propenso a los errores instantáneamente.

Por ejemplo, cuando accedes a los campos name o email, podrías rápidamente introducir un error tipográfico. Un error tipográfico que el compilador no conoce desde que el JSON vive en una estructura de mapa.

Serializar un JSON en una clase modelo

Combate los problemas mencionados previamente introduciendo una clase de modelo plana, llamada User en este ejemplo. Dentro de la clase User, encontrarás:

  • Un constructor User.fromJson(), para construir una nueva instancia de User desde una estructura de mapa.
  • Un método toJson(), que convierte una instancia User en un mapa.

Con esta aproximación, el calling code, puede tener seguridad de tipos, autocompletado para los campos name y email, y excepciones en tiempo de compilación. Si cometes errores tipográficos o tratas los campos como int en lugar de String, la app no compilará, en lugar de fallar en tiempo de ejecución.

user.dart

class User {
  final String name;
  final String email;

  User(this.name, this.email);

  User.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        email = json['email'];

  Map<String, dynamic> toJson() =>
    {
      'name': name,
      'email': email,
    };
}

La responsabilidad de la lógica de decodificación esta ahora situada dentro del propio modelo. Con esta aproximación, puedes decodificar un usuario fácilmente.

Map userMap = jsonDecode(jsonString);
var user = new User.fromJson(userMap);

print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');

Para codificar un usuario, pasa el objeto User al método jsonEncode(). No necesitas llamar al método toJson(), porque jsonEncode() ya hace esto por ti.

String json = jsonEncode(user);

Con esta aproximación, el código ejecutable no tiene que preocuparse acerca de la serialización JSON para nada. Sin embargo, la clase modelo definitivamente debe hacerlo. En una app en producción, querrás asegurarte que la serialización trabaja correctamente. En la práctica, los métodos User.fromJson() y User.toJson() necesitan ambos tener test unitarios para verificar un comportamiento correcto.

Sin embargo, los escenarios del mundo real no son normalmente tan simples. Es poco probable que use respuestas JSON tan pequeñas. También se usan habitualmente Objetos JSON anidados.

Sería bueno que hubiese algo que manejar la codificación y decodificación JSON por ti. Afortunadamente, ¡lo hay!

Serializar JSON usando librerías de auto-generación de código

Aunque hay otras bibliotecas disponibles, esta guía usa el paquete json_serializable, un generador de código automatizado que genera el boilerplate del JSON serializado por ti.

Dado que el código de serialización no esta más, escrito ni mantenido manualmente, minimizas el riesgo de tener excepciones por la serialización JSON en tiempo de ejecución.

Configurando json_serializable en un proyecto

Para incluir json_serializable en tu proyecto, necesitas una dependencia regular, y dos dev dependencies. Abreviando, dev dependencies son dependencias que no están incluidas en el código fuente de tu app—estas solo son usadas en el entorno de desarrollo.

Las últimas versiones de estas dependencias requeridas pueden verse en el siguiente fichero pubspec en el ejemplo de serialización JSON.

pubspec.yaml

dependencies:
  # Tus otras dependencias regulares aquí
  json_annotation: ^2.0.0

dev_dependencies:
  # Tus otras dev_dependencies aquí
  build_runner: ^1.0.0
  json_serializable: ^2.0.0

Ejecuta flutter pub get dentro de la carpeta raíz de tu proyecto (o haz clic en Packages Get en tu editor) para hacer estas nuevas dependencias disponibles en tu proyecto.

Creando las clases del modelo a la manera de json_serializable

Lo siguiente muestra como convertir una clase User en una clase json_serializable. En aras de la simplicidad, este código usa el modelo JSON simplificado de los ejemplos previos.

user.dart

import 'package:json_annotation/json_annotation.dart';

/// Esto permite a la clase `User` acceder a las propiedades privadas 
/// en el fichero generado. El valor para esto es *.g.dart, donde 
/// el asterisco denota el nombre del fichero fuente.
part 'user.g.dart';

/// Una anotación para el auto-generador de código para que sepa que en esta clase
/// necesita generarse lógica de serialización JSON.
@JsonSerializable()

class User {
  User(this.name, this.email);

  String name;
  String email;

  /// Un método constructor de tipo factory es necesario para crear una nueva instancia User
  /// desde un mapa. Pasa el mapa al constructor auto-generado `_$UserFromJson()`.
  /// El constructor es nombrado después de la clase fuente, en este caso User.
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

  /// `toJson` es la convención para una clase declarar que soporta serialización
  /// a JSON. La implementación simplemente llama al método de ayuda privado 
  /// `_$UserToJson`.
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

Con esta configuración, el auto-generador de código fuente, genera código para codificar y decodificar los campos name y email desde JSON.

Si se necesita, es también fácil de personalizar la estrategia de nombrado. Por ejemplo, si la API devuelve objetos con with snake_case, y tú quieres usar lowerCamelCase en tus modelos, puedes usar la anotación @JsonKey con un parámetro nombre:

/// Dice a json_serializable como "registration_date_millis" debe ser
/// mapeado para este proyecto.
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;

Ejecutando la utilidad de auto-generación de código

Cuando creas las clases json_serializable por primera vez, obtendrás errores similares a los mostrados en la imagen abajo.

Warning del IDE cuando el código auto-generado para una clase modelo no existe aún.

Estos errores son completamente normales y es simplemente porque el código auto-generado para la clase modelo no existe aún. Para resolver esto, ejecuta el auto-generador de código que genera los boilerplate de serialización.

Hay dos maneras de ejecutar el auto-generador de código.

Auto-generación de código una única vez

Ejecutando flutter pub run build_runner build en la raíz del proyecto, generas el código de serialización JSON para tus modelos, siempre que sea necesario. Esto desencadena una única compilación que pasa por los ficheros fuente, elige los relevantes, y genera el código de serialización necesario para ellos.

Si bien, esto es conveniente, sería bueno no tener que ejecutar la compilación manualmente cada vez que haces cambios en tus clases modelo.

Auto-generación contínua de código

Un watcher hace nuestro proceso de auto-generación de código más conveniente. Este observa cambios en los ficheros de nuestro proyecto y compila automáticamente los archivos necesarios cuando se necesita. Inicia el watcher ejecutando flutter pub run build_runner watch en la raíz del proyecto.

Es seguro ejecutar el watcher una vez y dejarlo funcionando en segundo plano.

Consumiendo modelos json_serializable

Para decodificar una cadena JSON a la manera de json_serializable, no tienes que hacer ningún cambio a tu código anterior.

Map userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);

Lo mismo ocurre para codificar. La API a llamar es la misma que antes.

String json = jsonEncode(user);

Con json_serializable, puedes olvidarte de cualquier serialización manual en la clase User. El auto-generador de código fuente crea un fichero llamado user.g.dart, que tiene toda la lógica de serialización necesaria. Ya no tienes que escribir más, pruebas automatizadas para asegurar que la serailización funciona—ahora es responsabilidad de la biblioteca asegurar que la serialización funciona apropiadamente.

Referencias adicionales

Para más información, mira los siguientes recursos: