¿Tiene que ocuparse del formato Base64? Entonces esta página es perfecta para Ud. Utilice nuestra práctica herramienta en línea para codificar o decodificar sus datos.

Decodificación Base64 en Dart: una guía completa

Llega en una respuesta de API, dentro de una URL, o pegada en un ticket de soporte: una ristra larga de letras y dígitos con el ocasional +, /, - o _, y quizás un par de signos = colgando al final. Alguien lo llama Base64 y tú necesitas lo que hay dentro. Esta guía es la receta de Dart para recuperarlo. Una orientación rápida, porque la página de inicio repasa el formato a fondo: Base64 reescribe cada tres bytes de entrada como cuatro caracteres de un alfabeto de 64 caracteres, y añade uno o dos = de padding al final cuando el último bloque sale corto. Decodificar es la dirección de reducción de esa operación: entran cuatro caracteres, salen tres bytes, así que el resultado siempre ocupa un cuarto menos de espacio que la entrada.

La buena noticia: no hay nada que instalar. Base64 viene incluido en la librería dart:convert desde Dart 1.13 en 2015, y la API ha estado estable desde Dart 2.0 en 2018. Un solo import te da un decodificador rápido y estricto que lee tanto el alfabeto estándar como el URL-safe.

Un límite honesto: esta es la parte del decodificador de la historia. Aprenderás qué acepta y qué rechaza el decodificador, cómo funciona el padding, cómo convertir bytes de vuelta a texto sin mojibake, y dónde te encontrarás con Base64: en JWTs, data URIs, archivos, streams, correo electrónico, configuración y línea de comandos. La otra dirección, empaquetar bytes en una cadena, tiene su propia guía, enlazada al final de esta.

Las cuatro puertas hacia una única máquina estricta

Aquí tienes toda la superficie pública que usarás, todo dentro de dart:convert:

Entrada Qué es Cuándo usarla
base64Decode(source) Función de nivel superior, decodifica a un Uint8List Decodificación de cada día, casi siempre esta
base64.decode(source) El método de decodificación del codec, comportamiento idéntico Quieres el codec para fuse o transformaciones de streams
base64Url.decode(source) El método de decodificación del codec URL-safe La entrada estaba documentada como URL-safe (la máquina es la misma)
base64Url.normalize(source) Valida y repara una cadena y la devuelve con padding La entrada puede faltarle padding, mezclar alfabetos o usar escapes por ciento

Dos cosas que notar. Primero, las cuatro puertas llevan al mismo decodificador: una única máquina de estados estricta con una sola tabla de búsqueda. Segundo, la última fila no es un decodificador en absoluto. Es un taller de reparación, y se ganará a pulso su sitio la primera vez que aparezca un JWT sin padding o un valor de configuración a medio limpiar.

Tu primera decodificación

Noventa por ciento de la vida de decodificación cabe en cinco líneas. Aquí tienes el ejemplo más pequeño que muestra la forma completa del trabajo:

import 'dart:convert';
void main() {
  final bytes = base64Decode('TWFu');
  final text = utf8.decode(bytes);
  print(text); // Man
}

Tres frases sobre lo que acaba de pasar. Primero, el punto de entrada devuelve bytes, no texto: base64Decode devuelve un Uint8List, y es a propósito, porque el payload puede ser una frase, un JPEG o un hash, y ninguno debe tratarse igual antes de saber qué tienes. Segundo, el salto de bytes a texto es un paso separado y explícito con una codificación explícita, y ese es el paso donde "café" se convierte en mojibake si no tienes cuidado. Tercero, la cadena vacía es un valor de primera clase: base64Decode('') te da una lista de longitud cero sin excepción y sin dramas.

Lo que el decodificador acepta y lo que rechaza

El decodificador de Dart es estricto por diseño. El RFC 4648 dice que las implementaciones deben rechazar entradas con caracteres fuera del alfabeto, y Dart sigue esa lectura al pie de la letra: no saltar espacios, no ignorar saltos de línea, no segundas oportunidades. Cuando la entrada está mal, te llega un FormatException que muestra la entrada y señala el carácter exacto. Aquí tienes el comportamiento con los clásicos causantes de problemas:

Entrada Qué falla Error exacto
'SGVs bG8s' un espacio se coló FormatException: Invalid character (at character 5)
'SGVs\nbG8s' un salto de línea se coló FormatException: Invalid character (at character 5)
'SGVs$bG8s' el signo de dólar no está en el alfabeto FormatException: Invalid character (at character 5)
'Zm8' nada de padding FormatException: Invalid length, must be multiple of four (at character 4)
'Zm8==' dos caracteres de padding donde toca uno FormatException: Invalid padding character (at character 5)
'Zm=8' padding en mitad de los datos FormatException: Invalid encoding before padding (at character 3)
'Zm8=xx' basura después del padding FormatException: Invalid padding character (at character 5)
'Zé' un carácter no ASCII FormatException: Invalid character (at character 2)

La posición en el mensaje es un recuento de caracteres que empieza en uno, y la entrada se imprime justo debajo del puntero, así que biseccionar un payload corrupto va rápido. Una sorpresa agradable se esconde en la estrictez: el decodificador acepta ambos alfabetos. Un - o _ en mitad de una cadena estándar va bien, y un + o / en una cadena URL-safe también. La elección de alfabeto solo importa cuando eres tú quien produce el texto, no cuando lo estás leyendo.

Padding: lo innegociable

Aquí tienes la regla que sorprende a la mayoría: el decodificador de Dart exige el padding correcto. La longitud de la entrada debe ser un múltiplo de cuatro caracteres, y los signos = finales deben estar presentes en la cantidad exacta. No hay modo permisivo, no hay flag para aflojarlo, y no hay configuración que cambiar. Las razones son sólidas: decodificar sin padding es ambiguo en casos límite, y el RFC advierte que una decodificación liberal puede abrir un canal encubierto, así que la lectura estricta es la segura. Lo que significa en la práctica:

Entrada Resultado
'' un Uint8List vacío, sin error
'QQ==' 1 byte: A
'QUI=' 2 bytes: AB
'QUJD' 3 bytes: ABC
'Zm8' FormatException: longitud inválida
'Zm8==' FormatException: carácter de padding inválido

Cuando la entrada viene de un sistema que quita el padding, y los JWT están llenos de valores sin padding, el paso de reparación es una llamada a normalize. Valida la cadena, convierte los caracteres URL-safe al alfabeto estándar y añade el padding que falta:

import 'dart:convert';
void main() {
  final stripped = '-__--Q';
  final repaired = base64Url.normalize(stripped);
  print(repaired); // +//++Q==
  final bytes = base64Decode(repaired);
  print('decoded ${bytes.length} bytes'); // decoded 4 bytes
}

La sorpresa del signo de porcentaje

Esta es una original de Dart. Cuando el Base64 aparece en un data URI, algunas herramientas codifican por porcentaje el padding, escribiendo %3D en lugar de =, porque un = suelto puede significar "separador de parámetros" en la sintaxis de URLs. La mayoría de los lenguajes te pedirían hacer un unescape primero. El decodificador de Dart no: su tabla de búsqueda trata %3D como una forma nativa del carácter de padding, así que puedes pasarle el payload crudo:

import 'dart:convert';
void main() {
  final fromDataUri = 'SGVsbG8%3D';
  final bytes = base64Decode(fromDataUri);
  print(utf8.decode(bytes)); // Hello
}

El escape se acepta exactamente donde el padding es legal, que es la posición final. Pon %3D donde un = sería rechazado y será rechazado de la misma forma, y %25 falla en cambio en el chequeo de padding - % es el carácter de escape nativo del padding en Dart, así que el decodificador lo lee como un = escapado y rechaza el 2 con Invalid padding character. En la práctica, esto significa que un payload ;base64, copiado directamente de las herramientas de desarrollo de un navegador se decodifica sin ningún preprocesado, un truco pequeño pero genuinamente cómodo.

Base64 URL-safe

El RFC 4648 define un segundo alfabeto por una razón: el estándar tiene tres caracteres, +, / y =, que chocan con la sintaxis de URLs. El alfabeto URL-safe, llamado base64url en el RFC, cambia + por - y / por _, y a menudo también suelta el padding. Es el alfabeto de los JWT, los IDs de objetos, los enlaces compartibles y todo lo que vive dentro de una URL o un nombre de archivo.

En el lado de la decodificación, Dart te da una única respuesta: los dos alfabetos los lee la misma máquina. base64Decode y base64Url.decode son dos nombres para el mismo decodificador, así que el único trabajo real es el padding, porque los productores URL-safe muy a menudo lo envían sin él. Para eso es exactamente normalize:

import 'dart:convert';
void main() {
  final bytes = [0xfb, 0xff, 0xfe, 0xf9];
  final urlSafe = base64UrlEncode(bytes);
  print(urlSafe); // -__--Q==
  final repaired = base64Url.normalize(urlSafe.replaceAll('=', ''));
  print(repaired); // +//++Q==
  print(base64Decode(repaired).length); // 4
}

Dos trampas para dejarte. No hagas a mano un reemplazo de - a + antes de decodificar; es innecesario, y normalize ya hace la conversión de alfabeto cuando hace falta. Y no asumas que una cadena URL-safe llega sin padding: algunos productores conservan el padding, y el decodificador acepta ambas cosas, siempre que el padding sea correcto.

De bytes a texto: la decisión del charset

La decodificación Base64 te entrega bytes. Si esos bytes son texto, debes elegir la codificación que los convierta de vuelta en una String, y esa elección te toca hacerla explícitamente. El supuesto por defecto en los sistemas modernos es UTF-8, y utf8.decode es el caballo de batalla:

import 'dart:convert';
void main() {
  final payload = base64Encode(utf8.encode('Héllo Wörld'));
  final bytes = base64Decode(payload);
  print(utf8.decode(bytes)); // Héllo Wörld
  final legacy = base64Encode(latin1.encode('Héllo'));
  print(latin1.decode(base64Decode(legacy))); // Héllo
}

Cuando los bytes no son UTF-8 válido, utf8.decode lanza un FormatException, que es el comportamiento correcto, mucho mejor que un mojibake silencioso. Si sabes que los datos son texto heredado de un solo byte, usa la codificación que corresponda:

Codificación Cuándo usarla Decodifica con
utf8 Texto moderno, JSON, lo que sea en la web utf8.decode(bytes)
latin1 Datos occidentales heredados de un solo byte latin1.decode(bytes)
ascii Texto plano de 7 bits ascii.decode(bytes)

Una trampa merece su propio aviso: String.fromCharCodes no es un charset. Lee bytes como unidades de código UTF-16, así que si le pasas los bytes UTF-8 de Héllo imprime Héllo con toda la cara. Si ves ese patrón de mojibake en tu salida, la solución es casi siempre utf8.decode.

JWTs: leyendo el token

Un JSON Web Token es tres partes base64url unidas por puntos: cabecera, payload, firma. El Base64 se usa aquí por compacidad y seguridad en URLs, no por secreto. Cualquiera con el token puede leer la cabecera y el payload, y así es por diseño. Lo que verificas es la firma, con el secreto compartido o la clave pública del emisor. Decodificar las partes legibles en Dart requiere unas pocas líneas:

import 'dart:convert';
Map<String, dynamic> readJwtPayload(String token) {
  final parts = token.split('.');
  if (parts.length != 3) {
    throw FormatException('Not a compact JWT');
  }
  final padded = base64Url.normalize(parts[1]);
  final bytes = base64Decode(padded);
  return jsonDecode(utf8.decode(bytes)) as Map<String, dynamic>;
}
void main() {
  const token =
      'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'
      '.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkRhcnQgRGV2IiwiaWF0IjoxNTE2MjM5MDIyfQ'
      '.c2lnbmF0dXJl';
  print(readJwtPayload(token)['name']); // Dart Dev
}

Fíjate en la coreografía del padding: los JWT se construyen sin padding, así que una parte fallará en un base64Decode directo cada vez que su longitud no sea múltiplo de cuatro. (En el ejemplo de arriba la cabecera sale a 36 caracteres y se decodifica directamente; el payload son 74 caracteres y no.) La llamada a normalize hace la reparación uniforme sin importar la longitud. Dos avisos más. Decodificar no es verificar: comprobar la firma y la reclamación exp es un paso aparte y obligatorio, normalmente con el paquete crypto para los algoritmos HMAC. Y desconfía de los tokens que declaran alg: none; un parser que los acepte es una vulnerabilidad, no una función.

Data URIs: archivos disfrazados de URL

Un data URI, definido por el RFC 2397, es una URL cuyo payload son los datos en sí: data:image/png;base64, seguido de los bytes codificados. Existen para que los canales solo de texto - atributos HTML, reglas CSS, documentos JSON - puedan llevar binarios sin un archivo aparte. El Base64 es el formato de payload elegido porque la alternativa, la codificación por porcentaje, es mucho más larga para datos binarios.

Y Dart puede analizarlos de forma nativa: el soporte de data URIs está en dart:core desde 2016, así que no hace falta ninguna librería de URIs:

import 'dart:convert';
void main() {
  final uri = Uri.parse('data:image/png;base64,iVBORw0KGgo=');
  final data = uri.data!;
  print(data.mimeType); // image/png
  print(data.isBase64); // true
  print('decoded ${data.contentAsBytes().length} bytes');
  final textUri = Uri.parse('data:text/plain;base64,SGVsbG8sIERhcnQh');
  print(textUri.data!.contentAsString()); // Hello, Dart!
}

El objeto UriData te da el tipo MIME, el flag isBase64, el texto crudo del payload y el contenido decodificado como cadena o como bytes. Dos trampas: el tipo MIME declarado puede mentir, así que en código sensible a la seguridad comprueba los bytes mágicos reales; y los data URIs son para assets pequeños, porque el payload completo viaja dentro del documento que lo referencia.

Archivos: Base64 en disco

Los archivos Base64 aparecen en formatos de exportación, paquetes de aprovisionamiento y cualquier transferencia solo de texto que necesite llevar binarios. La receta es: leer el texto, aplanarlo, decodificar, escribir los bytes:

import 'dart:convert';
import 'dart:io';
Future<void> main() async {
  final encoded = await File('image.b64').readAsString();
  final flat = encoded.replaceAll(RegExp(r'\s+'), '');
  final bytes = base64Decode(flat);
  await File('image.png').writeAsBytes(bytes);
  print('wrote ${bytes.length} bytes');
}

Ese replaceAll está haciendo trabajo real. Los archivos de texto están llenos de saltos de línea, a menudo la envoltura MIME de 76 caracteres, y el decodificador estricto los rechaza, así que aplana primero. La expresión regular elimina cada carácter de espacio en blanco, que es exactamente lo que quieres para un archivo base64 puro. Si el archivo puede contener otras anotaciones, como una cabecera PEM, quítalas explícitamente antes de decodificar, y deja que los errores del decodificador atrapen lo que esté realmente corrupto.

HTTP y APIs

El Base64 en HTTP lleva dos disfraces. Primero, respuestas de API: un campo JSON que lleva binarios como cadena. Segundo, la cabecera Authorization: Basic, donde las credenciales se codifican en base64 con el alfabeto estándar y padding:

import 'dart:convert';
import 'package:http/http.dart' as http;
Future<void> main() async {
  final response = await http.get(
    Uri.parse('https://httpbin.org/get?attachment=TWFuIGlzIGhlcmU%3D&name=man.txt'),
  );
  final payload = jsonDecode(response.body) as Map<String, dynamic>;
  final args = payload['args'] as Map<String, dynamic>;
  final bytes = base64Decode(args['attachment'] as String);
  print('got ${bytes.length} bytes');
  final credentials = utf8.decode(base64Decode('b2N0b2NhdDpzZWNyZXQ='));
  print(credentials.split(':').first); // octocat
}

El paquete http es el cliente estándar, a un dart pub add http de distancia. Para la autenticación Basic decodificas la parte que va después del prefijo Basic . Dos trampas: algunas APIs envían valores URL-safe o sin padding donde la documentación dice base64, así que si la decodificación directa lanza error, pasa el valor primero por base64Url.normalize; y recuerda que la autenticación Basic es ofuscación, no protección, por eso solo debe ir en conexiones TLS.

Correo y MIME: el problema del salto de línea

El correo electrónico es el cliente más antiguo del base64. El MIME envuelve las líneas base64 en 76 caracteres - 76 más CRLF caben cómodamente en una pantalla de 80 columnas - y el RFC 2045 dice a los decodificadores que ignoren los saltos de línea. El decodificador de Dart no lo hace, a propósito: los rechaza. La solución es aplanar antes de decodificar:

import 'dart:convert';
List<int> decodeMimeBody(String wrapped) {
  final flat = wrapped.replaceAll(RegExp(r'\s+'), '');
  return base64Decode(flat);
}
void main() {
  const wrapped =
      'SGVsbG8gZnJvbSBhbiBlbWFpbCBhdHRhY2htZW50LCB3cmFwcGVkIGF0IDc2IGNoYXJhY3RlcnMg'
      '\r\n'
      'dGhlIHdheSBNSU1FIHdhbnRzIGl0IHRvIGJlLCB3aXRoIENSTEYgYmV0d2VlbiB0aGUgbGluZXMu';
  print(utf8.decode(decodeMimeBody(wrapped)));
}

La regla es simple: quita los espacios en blanco, nada más. No quites otros caracteres con la esperanza de ser útil; el decodificador es el validador, y quieres que se queje de la corrupción real. Si procesas correo a escala, el paso de aplanar es barato, un solo paso de expresión regular, y mantiene el resto del pipeline honesto.

Configuración y variables de entorno

Los tokens y credenciales que viven en configuración basada en texto a veces se codifican en base64 para mantenerlos en una línea y que parezcan tokens. La formulación honesta: base64 es ofuscación, no cifrado, así que este patrón es por orden, nunca por secreto. El patrón en sí es trivial:

import 'dart:convert';
import 'package:dotenv/dotenv.dart';
Future<void> main() async {
  final env = DotEnv()..load();
  final encoded = env['API_TOKEN_B64'];
  if (encoded == null) {
    return;
  }
  final token = utf8.decode(base64Decode(encoded));
  print('loaded a ${token.length}-char token');
}

Con el paquete dotenv, el valor vive en un archivo .env como API_TOKEN_B64=c2stbGl2ZS1hYmMxMjM= y vuelve como texto plano después de la decodificación. La misma forma funciona con String.fromEnvironment para valores dart-define en tiempo de compilación, con un aviso: los valores dart-define quedan horneados en el binario compilado, así que lo que sea secreto debe vivir en configuración de tiempo de ejecución o en un gestor de secretos, no allí.

Streams: bloque a bloque

Cuando el texto codificado llega a trozos - un stream de red, un archivo grande leído por bloques - el decodificador lo maneja. Su máquina de estados lleva el grupo parcial más allá de los límites entre bloques, así que los bloques no necesitan alinearse en fronteras de cuatro caracteres:

import 'dart:convert';
Future<void> main() async {
  final incoming = Stream.fromIterable(['TWF', 'uaGVsbG8=']);
  final text = await incoming
      .transform(base64.decoder)
      .map(utf8.decode)
      .join();
  print(text); // Manhello
}

La llamada a transform usa el decodificador como transformador de streams; el primer bloque, tres caracteres, aparcó sus bits en el estado del decodificador, y el segundo bloque completa el grupo. Los errores aparecen como errores de stream con los mismos detalles de FormatException, y un stream vacío simplemente no produce salida. Si prefieres sinks, base64.decoder.startChunkedConversion te da un StringConversionSink cableado a la misma máquina de estados.

Big data: la matemática y la memoria

Decodificar reduce: cuatro caracteres se convierten en tres bytes, así que la salida siempre es un poco menos de tres cuartas partes de la longitud de la entrada. Eso significa que el tamaño de la salida se puede conocer antes de decodificar, lo que hace la memoria predecible. Un pequeño helper lo calcula solo con la cadena:

import 'dart:convert';
int decodedLength(String encoded) {
  var padding = 0;
  for (var i = encoded.length - 1; i >= 0 && padding < 2; i--) {
    if (encoded.codeUnitAt(i) == 0x3d) {
      padding++;
    } else {
      break;
    }
  }
  return (encoded.length ~/ 4) * 3 - padding;
}
void main() {
  print(decodedLength('QQ==')); // 1
  print(decodedLength('QUI=')); // 2
  print(decodedLength('QUJD')); // 3
}

El decodificador integrado es rápido: un solo paso sobre una tabla de búsqueda sin asignaciones de cadena por carácter, así que las cadenas de varios megabytes son cosa de todos los días. Donde base64 sí te cuesta es en el lado de la entrada: el texto codificado es unos 33 por ciento más grande que los datos, y es una cadena, que en la VM vive como unidades de código UTF-16, aproximadamente el doble de la longitud en bytes de los caracteres codificados. Para payloads que pueden crecer mucho, decodifica por stream en vez de unir una gran cadena.

Desde la línea de comandos

La VM de Dart convierte el decodificador en una CLI limpia. Esta pequeña herramienta lee un argumento de archivo o la entrada estándar, aplana los espacios en blanco y escribe bytes crudos a la salida estándar:

import 'dart:convert';
import 'dart:io';
Future<void> main(List<String> args) async {
  String encoded;
  if (args.isNotEmpty) {
    encoded = await File(args[0]).readAsString();
  } else {
    encoded = await stdin
        .transform(utf8.decoder)
        .join();
  }
  final flat = encoded.replaceAll(RegExp(r'\s+'), '');
  stdout.add(base64Decode(flat));
  await stdout.flush();
}

Guárdalo como bin/decode.dart y ejecuta dart run bin/decode.dart image.b64 > image.png, o pásalo por tubo: cat token.b64 | dart run bin/decode.dart. La llamada a stdout.add toma el Uint8List directamente, sin cadena intermedia, que es exactamente cómo debería moverse el binario a través de un pipeline.

Trampas que muerden a los desarrolladores de Dart

  • El muro del padding. La entrada estilo JWT y de herramientas de URL a menudo llega sin signos =, y el decodificador la rechaza con Invalid length, must be multiple of four. Pasa la entrada no fiable primero por base64Url.normalize.
  • La trampa de los espacios en blanco. Archivos de texto, correo y copiar-pegar introducen saltos de línea, y el decodificador nunca los salta. Aplana con replaceAll(RegExp(r'\s+'), '') antes de decodificar.
  • Confianza en el alfabeto. Como los dos alfabetos se decodifican en todas partes, no construyas lógica sobre qué decodificador produjo una cadena. La cadena es el contrato, no la configuración del productor.
  • String.fromCharCodes no es un charset. Lee unidades de código UTF-16, así que convierte texto UTF-8 en mojibake. Usa utf8.decode o una codificación explícita.
  • Dos tipos de error distintos. Los problemas de decodificación son FormatException; el codificador lanza ArgumentError para valores fuera del rango 0 a 255. Atrápalos por separado si estás construyendo una frontera de confianza.
  • El resultado es de longitud fija. Uint8List no puede crecer, así que bytes.add(1) lanza un UnsupportedError. Copia con List<int>.from(bytes) cuando necesites una lista crecible.
  • No des-escapes %3D a mano. El decodificador lee el padding escapado por porcentaje de forma nativa; un replaceAll('%3D', '=') prematuro acopla tu código a un detalle que el SDK ya gestiona.
  • Decodificar el payload de un JWT no es verificarlo. Leer las reclamaciones y confiar en ellas es un bug de seguridad esperando a un usuario determinado.

Buenas prácticas, lista corta

  • Por defecto usa base64Decode; acude a normalize solo en la frontera donde la entrada no es de fiar.
  • Sé explícito con el charset usando utf8.decode(bytes), incluso cuando das por hecho que es UTF-8.
  • Mantén los bytes como bytes hasta saber qué son; el Uint8List viaja limpio hacia File.writeAsBytes y compañía.
  • En las fronteras de confianza, atrapa FormatException y registra la posición de la entrada que te da el mensaje.
  • Pasa por stream cualquier cosa que pueda superar unos pocos megabytes.
  • Trata el base64 como un formato, no como una protección: no oculta nada a nadie que sepa que es base64.

Una breve historia del Base64 en Dart

El decodificador que acabas de conocer lleva aquí más tiempo que Dart 3, la null safety y la era de Flutter. La versión corta:

  • 18 de noviembre de 2015, Dart 1.13: el Base64 llega a dart:convert como la constante BASE64 más las clases Base64Codec, Base64Encoder y Base64Decoder. Antes de esta versión, el SDK no tenía base64 en absoluto.
  • 28 de enero de 2016, Dart 1.14: Base64Decoder.convert gana los parámetros de rango start y end, y la misma versión añade el soporte de data URIs a dart:core, el camino de Uri.parse en el que se apoya este artículo.
  • 26 de abril de 2016, Dart 1.16: el alfabeto URL-safe se une como BASE64URL y el constructor Base64Codec.urlSafe.
  • 7 de agosto de 2018, Dart 2.0: las constantes se renombran a minúsculas base64 y base64Url, llegan la base64Decode de nivel superior y compañía, la decodificación devuelve un Uint8List en lugar de una List<int> crecible, y Base64Codec.normalize se une a la familia, convirtiendo validación y reparación en un paso de una sola llamada.
  • 2021, Dart 2.12: llega la null safety, y toda la historia de dart:convert, base64 incluido, se vuelve null-safe.
  • Hoy, Dart 3.13: las clases están marcadas final, y el comportamiento que viste arriba es la misma máquina estricta, de ambos alfabetos y consciente del porcentaje, que funciona desde 2015.

La estrictez no es un accidente de la implementación. Es el decodificador siguiendo la instrucción del RFC 4648 de que las implementaciones deben rechazar caracteres fuera del alfabeto, dejando la tolerancia estilo MIME para las aplicaciones que la necesitan, lo que en Dart significa un paso de aplanar antes de la decodificación.

Datos curiosos

  • El decodificador lee %3D como padding nativo. Pásale el payload crudo de un data URI, escape y todo, y lo decodifica. Muy pocos runtimes de lenguaje lo harían sin un paso de preprocesado.
  • base64.decoder y base64Url.decoder son literalmente el mismo objeto: ambos son la instancia canónica const Base64Decoder(). El "decodificador URL-safe" es el decodificador estándar con otro disfraz.
  • Toda la máquina del decodificador cabe en una tabla de búsqueda de 128 entradas, un Int8List compartido entre el intérprete y el código compilado AOT, con + y - apuntando ambos a la posición 62 del alfabeto y / y _ apuntando ambos a la 63.
  • El base64 de Dart y su soporte de data URIs llegaron en dos versiones, la 1.13 y la 1.14, y claramente se planearon como pareja: uno para leer el formato, otro para leerlo directamente de una URL.
  • La cadena vacía se decodifica a un Uint8List vacío sin error, y la cadena vacía se codifica a la cadena vacía: base64 trata la ausencia de datos como un mensaje perfectamente válido.
  • En 2018, cuando Dart 2.0 renombró sus constantes, BASE64 pasó a ser base64 como parte de una migración de todo el SDK a nombres de constantes en minúsculas, la misma ola que te dio ascii, json y utf8.

Ya tienes el decodificador completo: qué acepta, qué rechaza, cómo reparar una entrada dañada y cómo encontrártelo en JWTs, data URIs, archivos, streams, correo y la shell. La otra dirección de la operación, tomar bytes y producir uno de los dos alfabetos, con las decisiones de padding y la matemática del tamaño, se cubre en detalle en la guía de codificación Base64, enlazada al final de esta página.

Última actualización: 2026-09-08

Artículo relacionado: Codificación Base64 en Dart: una guía completa