La transformación de datos entre objetos de Dart y cadenas de texto JSON se gestiona mediante la librería dart:convert. En términos precisos, es un proceso de traducción entre la estructura de memoria de la máquina virtual de Dart y un formato de intercambio de texto basado en pares clave-valor.
Este mecanismo funciona así porque Dart es un lenguaje de tipado fuerte y estático, mientras que JSON es un formato de texto sin esquema definido. Al decodificar, la función jsonDecode no puede adivinar la estructura de tus clases, por lo que devuelve un tipo dynamic, que puede ser un Map<String, dynamic>, una List, o un tipo primitivo. Si intentas decodificar un JSON malformado, el motor lanzará una FormatException; si asumes que un campo es un int cuando el JSON contiene un String, el sistema lanzará un TypeError en tiempo de ejecución. Debes utilizar este proceso siempre que interactúes con servicios externos, leas archivos de configuración o necesites persistir el estado de una aplicación.
import 'dart:convert';
// Para manejar objetos complejos, implementamos el patrón toJson/fromJson.
// Dart no tiene reflexión en tiempo de ejecución (como Java) para evitar
// un overhead excesivo en AOT, por lo que la serialización debe ser explícita.
class User {
final int id;
final String name;
final DateTime createdAt;
User({required this.id, required this.name, required this.createdAt});
// Convierte el objeto a un mapa serializable.
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'createdAt': createdAt.toIso8601String(),
};
// Constructor de fábrica para reconstruir el objeto desde un mapa.
// Nota: El tipo de 'createdAt' dependerá de si usamos un 'reviver' o no.
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int,
name: json['name'] as String,
createdAt: json['createdAt'] as DateTime,
);
}
@override
String toString() => 'User(id: $id, name: $name, createdAt: $createdAt)';
}
void main() {
final user = User(id: 101, name: 'Sofía', createdAt: DateTime.now());
// 1. Codificación (Encoding)
// Usamos 'toEncodable' para decirle a jsonEncode cómo tratar objetos
// que no son tipos básicos de JSON (como nuestro objeto User o DateTime).
final jsonString = jsonEncode(user, toEncodable: (obj) {
if (obj is User) return obj.toJson();
if (obj is DateTime) return obj.toIso8601String();
return obj;
});
// 2. Pretty Print
// JsonEncoder.withIndent permite generar un String con indentación para lectura humana.
final encoder = JsonEncoder.withIndent(' ');
print('JSON con formato:\n${encoder.convert(user.toJson())}');
// 3. Decodificación (Decoding) con 'reviver'
// El parámetro 'reviver' intercepta cada par clave-valor durante el parseo.
// Esto nos permite transformar strings de fecha en objetos DateTime de inmediato.
final rawJson = '{"id": 101, "name": "Sofía", "createdAt": "2023-10-27T15:30:00Z"}';
final decodedMap = jsonDecode(rawJson, reviver: (key, value) {
if (key == 'createdAt' && value is String) {
return DateTime.parse(value);
}
return value;
});
// Como el reviver ya convirtió el String a DateTime, la carga es directa.
final userFromParsed = User.fromJson(decodedMap as Map<String, dynamic>);
print('Objeto reconstruido: $userFromParsed');
// 4. Manejo de errores
try {
jsonDecode('{"key": invalid_value}');
} on FormatException catch (e) {
print('Error de formato detectado: ${e.message}');
}
}
Análisis del flujo de datos
En el ejemplo anterior, observa cómo gestionamos la transición entre tipos. Cuando ejecutamos jsonEncode pasando el objeto user, el motor de Dart no sabe cómo convertir una instancia de User a texto por sí mismo. Aquí es donde entra toEncodable: esta función actúa como un interceptor que, al encontrar un tipo no compatible (como User), ejecuta la lógica que definimos para devolver un Map.
Para la decodificación, la magia ocurre en reviver. Normalmente, jsonDecode vería "2023-10-27T15:30:00Z" y lo trataría simplemente como un String. Sin embargo, al interceptar la clave createdAt, transformamos ese valor en un objeto DateTime antes de que el Map final sea entregado. Esto permite que, al llamar a User.fromJson, el cast json['createdAt'] as DateTime sea seguro y no falle.
Finalmente, el uso de JsonEncoder.withIndent(' ') es la forma estándar de generar outputs legibles para logs o archivos de configuración, delegando la complejidad de la indentación al componente JsonEncoder de la librería, en lugar de manipular strings manualmente.
El error frecuente
Un error clásico al trabajar con jsonDecode es asumir que el tipo de dato devuelto coincide con el esquema esperado sin realizar validaciones o castings seguros.
// JSON malformado en tipo: "id" es un String, no un int
final badData = '{"id": "101", "name": "Error"}';
final map = jsonDecode(badData);
// Esto lanzará un TypeError en tiempo de ejecución porque el mapa
// contiene un String donde la clase espera un int.
final user = User.fromJson(map);
Para evitar esto, siempre debes validar los tipos o utilizar herramientas de generación de código que implementen transformaciones más robustas.
N° 83