JSON a Dart
Genera clases Dart desde JSON con fromJson y toJson
Entrada JSON
Salida Dart
¿Qué es la conversión de JSON a Dart?
La conversión de JSON a Dart toma un objeto JSON en bruto y produce definiciones de clases Dart con campos tipados, un constructor con nombre, una factory fromJson y un método toJson. Dart no tiene reflexión en tiempo de ejecución en Flutter (dart:mirrors está deshabilitado), por lo que no puedes deserializar JSON en objetos tipados sin escribir código de mapeo explícito. Cada respuesta de API REST, documento de Firebase o payload de configuración necesita una clase modelo Dart correspondiente antes de poder acceder a sus campos con seguridad de tipos.
Una clase modelo Dart típica para JSON declara campos final por cada clave, un constructor con parámetros con nombre (usando la palabra clave required para los campos no nulables), una factory constructor llamada fromJson que lee desde un Map de String a dynamic, y un método toJson que devuelve un Map de String a dynamic. Los objetos JSON anidados se convierten en clases separadas. Los arrays se convierten en campos List tipados. Los valores JSON nulables usan la sintaxis null-safety de Dart con el sufijo ? en el tipo.
Escribir estas clases modelo a mano implica leer cada clave JSON, decidir el tipo Dart, crear el cast de fromJson para cada campo (incluyendo el mapeo de listas con .map().toList()), construir el mapa literal de toJson y repetir el proceso para cada objeto anidado. Para un objeto JSON con 12 campos y 2 objetos anidados, eso significa 3 clases, 6 líneas de factory y decenas de expresiones de cast. Un conversor produce todo esto en milisegundos a partir de un único pegado.
¿Por qué usar un conversor de JSON a Dart?
Escribir clases modelo Dart desde JSON de forma manual implica leer nombres de campo, inferir tipos a partir de valores de ejemplo, escribir casts fromJson con manejo correcto de nulos y repetir el proceso para los objetos anidados. Cuando cambia la estructura de la API, cada actualización de campo toca el constructor, el fromJson y el toJson. Un conversor elimina ese trabajo repetitivo.
Casos de uso de JSON a Dart
Mapeo de tipos JSON a Dart
Cada valor JSON se mapea a un tipo Dart específico. La tabla a continuación muestra cómo el conversor traduce cada tipo JSON. La columna Alternativa muestra los tipos usados en escenarios menos comunes o de mapeo manual.
| Tipo JSON | Ejemplo | Tipo Dart | Alternativa |
|---|---|---|---|
| string | "hello" | String | String |
| number (integer) | 42 | int | int |
| number (float) | 3.14 | double | double |
| boolean | true | bool | bool |
| null | null | dynamic | Null / dynamic |
| object | {"k": "v"} | NestedClass | Map<String, dynamic> |
| array of strings | ["a", "b"] | List<String> | List<String> |
| array of objects | [{"id": 1}] | List<Item> | List<Item> |
| mixed array | [1, "a"] | List<dynamic> | List<dynamic> |
Enfoques de serialización JSON en Dart
Dart y Flutter ofrecen múltiples formas de gestionar la serialización JSON. El enfoque manual con fromJson/toJson es el más sencillo y no requiere generación de código. Para proyectos más grandes, las soluciones basadas en build_runner como json_serializable y freezed generan el boilerplate en tiempo de compilación, reduciendo errores y manteniendo los modelos coherentes con el contrato JSON.
| Enfoque | Descripción | Fuente |
|---|---|---|
| json_serializable | Code-generation-based JSON serialization. Generates .g.dart files at build time via build_runner. Type-safe and compile-time verified. | Official (Google) |
| freezed | Generates immutable data classes with copyWith, fromJson/toJson, equality, and pattern matching support. Built on top of json_serializable. | Community (rrousselGit) |
| built_value | Generates immutable value types with serialization. Enforces immutability patterns. Used in larger codebases with strict data modeling. | |
| dart_mappable | Annotation-based mapper that generates fromJson, toJson, copyWith, and equality. Simpler setup than freezed with similar features. | Community |
| Manual fromJson/toJson | Hand-written factory constructors and toJson methods. No code generation needed. Full control over the mapping logic. | Built-in Dart |
Manual vs json_serializable vs freezed
Dart tiene tres enfoques comunes para las clases modelo JSON. El fromJson/toJson manual no tiene dependencias. json_serializable automatiza el código de mapeo. freezed añade inmutabilidad, copyWith y pattern matching sobre json_serializable.
Ejemplos de código
Estos ejemplos muestran cómo usar las clases Dart generadas para deserialización JSON, cómo configurar json_serializable con build_runner y cómo generar clases Dart de forma programática desde JavaScript y Python.
import 'dart:convert';
class User {
final int id;
final String name;
final String email;
final bool active;
final Address address;
final List<String> tags;
User({
required this.id,
required this.name,
required this.email,
required this.active,
required this.address,
required this.tags,
});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
active: json['active'] as bool,
address: Address.fromJson(json['address'] as Map<String, dynamic>),
tags: (json['tags'] as List<dynamic>).map((e) => e as String).toList(),
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
'active': active,
'address': address.toJson(),
'tags': tags,
};
}
class Address {
final String street;
final String city;
final String zip;
Address({required this.street, required this.city, required this.zip});
factory Address.fromJson(Map<String, dynamic> json) => Address(
street: json['street'] as String,
city: json['city'] as String,
zip: json['zip'] as String,
);
Map<String, dynamic> toJson() => {'street': street, 'city': city, 'zip': zip};
}
void main() {
final jsonStr = '{"id":1,"name":"Alice","email":"alice@example.com","active":true,"address":{"street":"123 Main","city":"Springfield","zip":"12345"},"tags":["admin","user"]}';
final user = User.fromJson(jsonDecode(jsonStr));
print(user.name); // -> Alice
print(jsonEncode(user.toJson())); // -> round-trip back to JSON
}// pubspec.yaml dependencies:
// json_annotation: ^4.8.0
// dev_dependencies:
// build_runner: ^2.4.0
// json_serializable: ^6.7.0
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; // generated by: dart run build_runner build
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
@JsonKey(name: 'is_active')
final bool isActive;
final List<String> tags;
User({
required this.id,
required this.name,
required this.email,
required this.isActive,
required this.tags,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
// Run code generation:
// dart run build_runner build --delete-conflicting-outputsfunction jsonToDart(obj, name = "Root") {
const classes = [];
function infer(val, fieldName) {
if (val === null) return "dynamic";
if (typeof val === "string") return "String";
if (typeof val === "number") return Number.isInteger(val) ? "int" : "double";
if (typeof val === "boolean") return "bool";
if (Array.isArray(val)) {
const first = val.find(v => v !== null);
if (!first) return "List<dynamic>";
return `List<${infer(first, fieldName)}>`;
}
if (typeof val === "object") {
const cls = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
build(val, cls);
return cls;
}
return "dynamic";
}
function build(obj, cls) {
const fields = Object.entries(obj).map(([k, v]) =>
` final ${infer(v, k)} ${k};`
);
classes.push(`class ${cls} {\n${fields.join("\n")}\n}`);
}
build(obj, name);
return classes.join("\n\n");
}
console.log(jsonToDart({ id: 1, name: "Alice", scores: [98, 85] }, "User"));
// class User {
// final int id;
// final String name;
// final List<int> scores;
// }import json
def json_to_dart(obj: dict, class_name: str = "Root") -> str:
classes = []
def infer(val, name):
if val is None:
return "dynamic"
if isinstance(val, bool):
return "bool"
if isinstance(val, int):
return "int"
if isinstance(val, float):
return "double"
if isinstance(val, str):
return "String"
if isinstance(val, list):
if not val:
return "List<dynamic>"
return f"List<{infer(val[0], name)}>"
if isinstance(val, dict):
cls = name[0].upper() + name[1:]
build(val, cls)
return cls
return "dynamic"
def build(obj, cls):
fields = [f" final {infer(v, k)} {k};" for k, v in obj.items()]
classes.append(f"class {cls} {{\n" + "\n".join(fields) + "\n}")
build(obj, class_name)
return "\n\n".join(classes)
data = json.loads('{"id": 1, "name": "Alice", "active": true}')
print(json_to_dart(data, "User"))
# class User {
# final int id;
# final String name;
# final bool active;
# }