CULTIVA IA

Flutter: Serialización JSON Manual

CultivaShop — App marketplace B2B · Modelos: Servicio + Propuesta

📱 Flutter / Dart

⚡ Flujo de implementación

🏗️
Definir
clase modelo
⬇️
fromJson()
factory
⬆️
toJson()
método
🧪
Unit tests
round-trip

Payload Pequeño

Parse sincrónico en main thread con jsonDecode(). Adecuado para un único objeto como Servicio.

Payload Grande

Usa compute(parseFn, body) para mover el parse a un isolate y evitar jank en la UI (listas > 500 ítems).

Type Safety

Siempre castea el resultado de jsonDecode a Map<String, dynamic>. Lanza excepción si el cast falla.

servicio.dart Model
class Servicio { final String id; final String nombre; final String categoria; final double precioBase; final bool activo; final String fechaCreacion; const Servicio({ required this.id, required this.nombre, required this.categoria, required this.precioBase, required this.activo, required this.fechaCreacion, }); /// Deserialización con switch pattern (type-safe) factory Servicio.fromJson( Map<String, dynamic> json ) { return switch (json) { { 'id': final String id, 'nombre': final String nombre, 'precio_base': final num precio, 'activo': final bool activo, 'fecha_creacion': final String fecha, } => Servicio( id: id, nombre: nombre, categoria: json['categoria'] as String, precioBase: precio.toDouble(), activo: activo, fechaCreacion: fecha, ), _ => throw const FormatException( 'JSON de Servicio inválido.'), }; } /// Serialización: Servicio → JSON Map Map<String, dynamic> toJson() => { 'id': id, 'nombre': nombre, 'categoria': categoria, 'precio_base': precioBase, 'activo': activo, 'fecha_creacion': fechaCreacion, }; }
propuesta.dart Isolate-ready
class Propuesta { final String id; final String servicioId; final String cliente; final String estado; final double importe; final String fecha; final String notas; const Propuesta({ /* ... required params ... */ }); factory Propuesta.fromJson( Map<String, dynamic> json ) { return switch (json) { { 'id': final String id, 'servicio_id': final String svcId, 'cliente': final String cliente, 'estado': final String estado, 'importe': final num importe, 'fecha': final String fecha, 'notas': final String notas, } => Propuesta( id: id, servicioId: svcId, cliente: cliente, estado: estado, importe: importe.toDouble(), fecha: fecha, notas: notas, ), _ => throw const FormatException( 'JSON de Propuesta inválido.'), }; } Map<String, dynamic> toJson() => { 'id': id, 'servicio_id': servicioId, 'cliente': cliente, 'estado': estado, 'importe': importe, 'fecha': fecha, 'notas': notas, }; }
servicio_api.dart Sync Parse
/// Payload pequeño → parse en main thread Future<Servicio> fetchServicio( http.Client client, String id, ) async { final response = await client.get( Uri.parse( '$_baseUrl/api/servicios/$id'), headers: {'Accept': 'application/json'}, ); if (response.statusCode == 200) { // Cast explícito: dynamic → Map final Map<String, dynamic> map = jsonDecode(response.body) as Map<String, dynamic>; return Servicio.fromJson(map); } throw Exception( 'Error $id — HTTP ${response.statusCode}'); }
propuesta_api.dart Background Isolate
/// Top-level fn (requerida por compute()) List<Propuesta> parsePropuestas( String body, ) { final list = (jsonDecode(body) as List) .cast<Map<String, dynamic>>(); return list.map( (j) => Propuesta.fromJson(j) ).toList(); } /// Payload grande → isolate background Future<List<Propuesta>> fetchPropuestas( http.Client client, String clienteId, ) async { final res = await client.get( Uri.parse( '$_base/propuestas?cliente_id=$clienteId'), ); if (res.statusCode == 200) { // Evita UI jank en listas grandes ✅ return compute(parsePropuestas, res.body); } throw Exception( 'Error propuestas — HTTP ${res.statusCode}'); }
test_output.txt — Round-trip demo Test Output
Servicio deserializado: Servicio(svc-042, "Automatización de Email Marketing", 490.0€) → toJson: {id: svc-042, nombre: Automatización de Email Marketing, categoria: automatizacion, precio_base: 490.0, activo: true, fecha_creacion: 2025-09-01} Propuesta deserializada: Propuesta(prop-1001, "Café Origen SL", aprobada, 490.0€) Round-trip OK: Propuesta(prop-1001, "Café Origen SL", aprobada, 490.0€) objeto → toJson()jsonEncodejsonDecodefromJson → objeto idéntico ✔ Lista de 3 propuestas parseada (top-level fn lista para compute()) Apta para background isolate — sin bloquear main thread ✔ FormatException capturada correctamente: FormatException: JSON de Servicio inválido o incompleto. fromJson lanza excepción ante campos ausentes — no devuelve null ✔