Convert #

dart:convert is Dart’s built-in library for converting data between different formats — JSON, UTF-8, Base64, Latin1, ASCII, and HTML escaping. More than just jsonDecode and jsonEncode, this library provides Codec and Converter abstractions that can be flexibly composed, support streaming for large data, and can be extended with custom codecs. Fully understanding dart:convert unlocks the ability to handle complex, efficient data conversions.

An Overview of dart:convert #

flowchart LR
    DC["dart:convert"] --> JSON["JsonCodec\njsonEncode / jsonDecode"]
    DC --> UTF8["Utf8Codec\nutf8.encode / utf8.decode"]
    DC --> B64["Base64Codec\nbase64Encode / base64Decode"]
    DC --> LAT["Latin1Codec\nlatin1.encode / latin1.decode"]
    DC --> ASCII["AsciiCodec\nascii.encode / ascii.decode"]
    DC --> HTML["HtmlEscapeCodec\nhtmlEscape.convert"]
    DC --> CHAIN["Codec Chaining\ncodec.fuse(otherCodec)"]

JSON — JsonCodec #

import 'dart:convert';

// Encode — Dart object → JSON string
final map = {
  'nama': 'Budi',
  'umur': 25,
  'aktif': true,
  'nilai': null,
  'skor': [90, 85, 92],
  'alamat': {'kota': 'Jakarta'},
};

final jsonString = jsonEncode(map);
print(jsonString);
// {"nama":"Budi","umur":25,"aktif":true,"nilai":null,"skor":[90,85,92],...}

// Decode — JSON string → Dart object
final decoded = jsonDecode(jsonString) as Map<String, dynamic>;
print(decoded['nama']); // 'Budi'

// With indentation (for debugging/logging)
final jsonRapi = JsonEncoder.withIndent('  ').convert(map);
print(jsonRapi);
// {
//   "nama": "Budi",
//   "umur": 25,
//   ...
// }

// Alternative API — using a codec instance
final codec = json; // alias for JsonCodec()
print(codec.encode(map));
print(codec.decode(jsonString));

toEncodable — Encoding Non-Standard Types #

import 'dart:convert';

// Encode types that JSON doesn't support natively
final data = {
  'dibuat': DateTime.now(),          // DateTime can't be encoded directly
  'status': StatusOrder.aktif,       // Enums can't be encoded directly
  'url': Uri.parse('https://dart.dev'), // Uri can't be encoded directly
};

// ANTI-PATTERN: direct jsonEncode — throws JsonUnsupportedObjectError
// jsonEncode(data); // ✗ crash!

// CORRECT: toEncodable to handle non-standard types
final encoded = jsonEncode(data, toEncodable: (value) {
  if (value is DateTime) return value.toUtc().toIso8601String();
  if (value is Enum) return value.name;
  if (value is Uri) return value.toString();
  throw UnsupportedError('Unsupported type: ${value.runtimeType}');
});

print(encoded);
// {"dibuat":"2024-11-15T07:30:00.000Z","status":"aktif","url":"https://dart.dev"}

Streaming JSON — JsonEncoder and JsonDecoder #

For very large JSON, streaming is more efficient than plain strings:

import 'dart:convert';
import 'dart:io';

// Stream JSON to a file without loading everything into memory
Future<void> tulisJsonKefile(String path, List<Map<String, dynamic>> data) async {
  final file = File(path);
  final sink = file.openWrite();

  final encoder = JsonEncoder.withIndent('  ');

  // Write as JSON Lines (NDJSON) — one object per line
  for (final item in data) {
    sink.writeln(encoder.convert(item));
  }

  await sink.flush();
  await sink.close();
}

// Read NDJSON from a file with streaming
Future<void> bacaNdjson(String path) async {
  final file = File(path);
  await file
      .openRead()
      .transform(utf8.decoder)
      .transform(const LineSplitter())
      .map((baris) => jsonDecode(baris) as Map<String, dynamic>)
      .forEach((obj) => print(obj['nama']));
}

UTF-8 — Utf8Codec #

import 'dart:convert';

// Encode String → bytes (List<int>)
List<int> bytes = utf8.encode('Halo, Dunia! 🌍');
print(bytes.length); // larger than string.length because the emoji is 4 bytes

// Decode bytes → String
String teks = utf8.decode(bytes);
print(teks); // 'Halo, Dunia! 🌍'

// allowMalformed: true — ignore invalid byte sequences
String safe = utf8.decode(bytesRusak, allowMalformed: true);

// Streaming — encoders and decoders as StreamTransformers
import 'dart:io';

// Read a file as a Stream<String>
final stream = File('besar.txt')
    .openRead()
    .transform(utf8.decoder); // Stream<List<int>> → Stream<String>

// Write Strings as bytes
final sink = File('output.txt').openWrite();
sink.add(utf8.encode('Konten file'));

// Encoders and decoders as separate objects
final encoder = utf8.encoder; // Converter<String, List<int>>
final decoder = utf8.decoder; // Converter<List<int>, String>

print(encoder.convert('test')); // [116, 101, 115, 116]
print(decoder.convert([116, 101, 115, 116])); // 'test'

Encoding Comparison #

import 'dart:convert';

const teks = 'Halo! Résumé café naïve';

// UTF-8 — supports all Unicode
final utf8Bytes = utf8.encode(teks);
print('UTF-8: ${utf8Bytes.length} bytes');

// Latin-1 / ISO-8859-1 — only Latin characters (code points 0-255)
final latin1Bytes = latin1.encode('Halo! Resume cafe naive'); // without accents
print('Latin-1: ${latin1Bytes.length} bytes');

// ASCII — only 7-bit ASCII (0-127)
try {
  final asciiBytes = ascii.encode(teks); // ✗ throws if there are non-ASCII characters
} on ArgumentError catch (e) {
  print('ASCII error: $e');
}

// ASCII with handling
final asciiSafe = ascii.encode('Hello World'); // ✓ only ASCII

Base64 — Base64Codec #

Base64 converts binary data into ASCII text that’s safe for transport (HTTP headers, email, JSON):

import 'dart:convert';

// Encode bytes → Base64 string
final data = [72, 101, 108, 108, 111]; // 'Hello' in bytes
final b64 = base64Encode(data);
print(b64); // 'SGVsbG8='

// Decode a Base64 string → bytes
final decoded = base64Decode(b64);
print(decoded); // [72, 101, 108, 108, 111]
print(utf8.decode(decoded)); // 'Hello'

// Round-trip String → Base64 → String
String teks = 'Data rahasia: [email protected]';
String encoded = base64Encode(utf8.encode(teks));
print(encoded); // 'RGF0YSByYWhhc2lhOiBidWRpQGV4YW1wbGUuY29t'
String restored = utf8.decode(base64Decode(encoded));
print(restored); // 'Data rahasia: [email protected]'

// Base64Url — URL-safe version (uses - and _ instead of + and /)
final urlSafe = base64Url.encode(data);
print(urlSafe); // without + and / characters

// Base64 padding — some implementations omit the padding (=)
String tanpaPadding = base64Encode(data).replaceAll('=', '');
// Add padding before decoding if needed
String denganPadding = tanpaPadding.padRight(
  tanpaPadding.length + (4 - tanpaPadding.length % 4) % 4,
  '=',
);

Base64 Use Cases #

import 'dart:convert';
import 'dart:io';

// 1. HTTP Basic Authentication
String basicAuth(String username, String password) {
  final credentials = '$username:$password';
  return 'Basic ${base64Encode(utf8.encode(credentials))}';
}

print(basicAuth('admin', 'password'));
// 'Basic YWRtaW46cGFzc3dvcmQ='

// 2. Encode an image for a data URL
Future<String> imageToDataUrl(String imagePath) async {
  final bytes = await File(imagePath).readAsBytes();
  final b64 = base64Encode(bytes);
  return 'data:image/png;base64,$b64';
}

// 3. JWT tokens (header.payload.signature — each Base64Url)
Map<String, dynamic> decodeJwtPayload(String token) {
  final parts = token.split('.');
  if (parts.length != 3) throw ArgumentError('Invalid JWT token');

  // Add padding if needed
  String padded = parts[1];
  switch (padded.length % 4) {
    case 2: padded += '=='; break;
    case 3: padded += '='; break;
  }

  final decoded = utf8.decode(base64Url.decode(padded));
  return jsonDecode(decoded) as Map<String, dynamic>;
}

HTML Escaping — HtmlEscape #

import 'dart:convert';

// HtmlEscape.convert — escape dangerous HTML characters
const htmlEscape = HtmlEscape();

print(htmlEscape.convert('<script>alert("XSS")</script>'));
// '&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;'

print(htmlEscape.convert('5 > 3 && 2 < 4'));
// '5 &gt; 3 &amp;&amp; 2 &lt; 4'

// Different escape modes
const modeAtribut = HtmlEscape(HtmlEscapeMode.attribute);
const modeTeks = HtmlEscape(HtmlEscapeMode.element);
const modeUrl = HtmlEscape(HtmlEscapeMode.unknown);

// Use when inserting data into HTML
String renderKartuPengguna(String nama, String bio) {
  final escape = HtmlEscape();
  return '''
    <div class="kartu">
      <h2>${escape.convert(nama)}</h2>
      <p>${escape.convert(bio)}</p>
    </div>
  ''';
}

// ANTI-PATTERN: inserting user input directly into HTML — XSS vulnerability!
String html = '<p>Halo, $namaUser!</p>'; // ✗ if namaUser = '<script>...</script>'

// CORRECT: always escape user input
String html2 = '<p>Halo, ${htmlEscape.convert(namaUser)}!</p>'; // ✓

Codec Chaining — fuse #

One of the most powerful, rarely-known features of dart:convert: codecs can be composed with fuse():

import 'dart:convert';

// fuse — combine two codecs into one
// String → UTF-8 bytes → Base64 string
final utf8ToBase64 = utf8.encoder.fuse(base64.encoder);
final stringKeBase64 = utf8ToBase64.convert('Halo, Dunia!');
print(stringKeBase64); // 'SGFsbywgRHVuaWEh'

// Reverse: Base64 string → UTF-8 bytes → String
final base64ToUtf8 = base64.decoder.fuse(utf8.decoder);
final base64KeString = base64ToUtf8.convert('SGFsbywgRHVuaWEh');
print(base64KeString); // 'Halo, Dunia!'

// Chain JSON + UTF-8 — JSON string → bytes
final jsonToUtf8 = json.encoder.fuse(utf8.encoder);
final jsonBytes = jsonToUtf8.convert({'nama': 'Budi', 'umur': 25});
print(jsonBytes); // List<int> representing the JSON in UTF-8

// Decode back
final utf8ToJson = utf8.decoder.fuse(json.decoder);
final objek = utf8ToJson.convert(jsonBytes);
print(objek); // {'nama': 'Budi', 'umur': 25}

// Streaming with a codec chain
import 'dart:io';

// Read a file → decode UTF-8 → parse JSON lines
final stream = File('data.ndjson')
    .openRead()
    .transform(utf8.decoder)           // bytes → String
    .transform(const LineSplitter())   // String → per line
    .map(jsonDecode);                  // String → dynamic

Custom Codecs #

For conversion formats not available in dart:convert, create your own codec:

import 'dart:convert';

// Custom codec for a simple CSV format
class CsvCodec extends Codec<List<List<String>>, String> {
  const CsvCodec({this.separator = ','});
  final String separator;

  @override
  Converter<List<List<String>>, String> get encoder => CsvEncoder(separator);

  @override
  Converter<String, List<List<String>>> get decoder => CsvDecoder(separator);
}

class CsvEncoder extends Converter<List<List<String>>, String> {
  const CsvEncoder(this.separator);
  final String separator;

  @override
  String convert(List<List<String>> input) {
    return input
        .map((row) => row
            .map((cell) => cell.contains(separator)
                ? '"$cell"'  // wrap with quotes if it contains the separator
                : cell)
            .join(separator))
        .join('\n');
  }
}

class CsvDecoder extends Converter<String, List<List<String>>> {
  const CsvDecoder(this.separator);
  final String separator;

  @override
  List<List<String>> convert(String input) {
    return input
        .split('\n')
        .where((baris) => baris.isNotEmpty)
        .map((baris) => baris.split(separator))
        .toList();
  }
}

// Usage
void main() {
  const csv = CsvCodec();

  final data = [
    ['nama', 'umur', 'kota'],
    ['Budi', '25', 'Jakarta'],
    ['Siti', '30', 'Bandung'],
  ];

  final csvString = csv.encode(data);
  print(csvString);
  // nama,umur,kota
  // Budi,25,Jakarta
  // Siti,30,Bandung

  final decoded = csv.decode(csvString);
  print(decoded[1]); // ['Budi', '25', 'Jakarta']
}

Quick Reference #

CodecEncodeDecodeUse case
jsonjsonEncode(obj)jsonDecode(str)JSON serialization
utf8utf8.encode(str)utf8.decode(bytes)String ↔ bytes
latin1latin1.encode(str)latin1.decode(bytes)ISO-8859-1
asciiascii.encode(str)ascii.decode(bytes)7-bit ASCII
base64base64Encode(bytes)base64Decode(str)Binary ↔ text
base64Urlbase64Url.encode(bytes)base64Url.decode(str)URL-safe Base64
htmlEscapehtmlEscape.convert(str)Preventing XSS

Summary #

  • jsonEncode/jsonDecode are top-level functions that are shortcuts for json.encode/json.decode — both are equivalent.
  • The toEncodable parameter in jsonEncode lets you encode non-standard types like DateTime, Enum, and Uri — always handle these rather than letting JsonUnsupportedObjectError crash.
  • utf8 is the default codec for all modern I/O — HTTP, files, WebSocket, and Socket all use UTF-8. Use utf8.encode/utf8.decode for String ↔ bytes conversions.
  • base64 vs base64Url — use base64Url for values going into URLs or JWT tokens because it doesn’t contain + and / characters that need URL encoding.
  • fuse() for composing codecs — utf8.encoder.fuse(base64.encoder) produces a converter straight from String to Base64 without intermediate variables.
  • HtmlEscape is mandatory when inserting user data into HTML — it prevents XSS injection, a serious security vulnerability.
  • Streaming codecs (transform()) are more efficient for large files — openRead().transform(utf8.decoder) reads large files without loading them entirely into memory.
  • JsonEncoder.withIndent(' ') for human-readable JSON output — use when debugging or creating configuration files.
  • utf8.decoder with allowMalformed: true to handle possibly corrupted binary data — without this flag, invalid UTF-8 byte sequences throw a FormatException.
  • Custom Codecs by extending Codec<S, T> let you create custom conversion formats integrated with dart:convert’s streaming and fuse system.

← Previous: Async   Next: Collection →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact