| |

Dart 47 🎯 JSON Serialization and Deserialization using dart:convert

JSON is the wire format of the modern web. The REST APIs return it, the configuration files store it, the WebSocket messages carry it, and the local storage persists it. The dart:convert library is the built-in codec for the JSON, and it provides the two functions: the jsonEncode and the jsonDecode. The jsonEncode takes the Dart object and returns the JSON string, and the jsonDecode takes the JSON string and returns the Dart object. The functions are the foundation, and the pattern is the mapping between the dynamic JSON structure and the typed Dart classes.

The mapping is not automatic. The jsonDecode returns the dynamic — the Map<String, dynamic> for the objects, the List<dynamic> for the arrays, the String, the num, the bool, and the null for the primitives. The Dart class does not know how to construct itself from the map, and the developer writes the fromJson factory and the toJson method. The manual mapping is the explicit and the predictable, and it is the pattern that the dart:convert provides. The json_serializable package is the code-generation alternative, and it generates the boilerplate, but the manual approach is the foundation.

The types are the subtlety. The JSON has the number which is the int or the double, and the Dart separates them. The JSON has the null, and the Dart’s null safety distinguishes the nullable from the non-nullable. The JSON has the nested objects and the arrays, and the Dart maps them to the nested classes and the List<T>. The DateTime is not the JSON type, and the convention is the ISO 8601 string or the Unix timestamp. The enum is not the JSON type, and the convention is the string name or the integer value.

This chapter covers three areas. First, why the dart:convert exists — the JSON-codec problem and the manual-mapping approach. Second, the encoding and the decoding — the jsonEncode, the jsonDecode, the JsonEncoder, the JsonDecoder, and the toEncodable. Third, the patterns and the errors — the fromJson factory, the toJson method, the nested objects, the lists, the enums, the DateTime, the null safety, and the FormatException. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the JSON-to-Dart mapping.

Key point: The jsonEncode converts the Dart objects into the JSON string, and the jsonDecode parses the JSON string into the Dart objects. The jsonDecode returns the dynamic — the Map<String, dynamic>, the List<dynamic>, the String, the num, the bool, and the null. The developer writes the fromJson factory and the toJson method for the typed classes. The JsonEncoder and the JsonDecoder are the configurable codecs. The FormatException is the malformed JSON error.


Why dart:convert exists

The JSON-codec problem. The JSON is the text format, and the Dart is the object format. The bridge between them is the codec. The dart:convert library provides the jsonEncode and the jsonDecode functions, and they are the built-in codec. The jsonEncode walks the Dart object and writes the JSON, and the jsonDecode reads the JSON and constructs the Dart object. The codec is the foundation of the data interchange.

The dynamic problem. The jsonDecode returns the dynamic, because the JSON is the untyped. The Map<String, dynamic> is the object, the List<dynamic> is the array, and the primitives are the String, the num, the bool, and the null. The dynamic means the compiler does not check the access, and the runtime error is the possibility. The typed class with the fromJson factory restores the type safety.

The manual-mapping problem. The Dart does not have the reflection in the Flutter and the AOT-compiled builds, so the automatic mapping is not possible without the code generation. The manual fromJson and toJson are the explicit and the predictable. The json_serializable package generates them from the annotations, and the generated code is the boilerplate. The manual approach is the foundation, and the code generation is the convenience.

The type-mapping problem. The JSON types do not map directly to the Dart types. The number is the int or the double, and the Dart distinguishes them. The DateTime is not the JSON type, and the convention is the ISO 8601 string. The enum is not the JSON type, and the convention is the string or the integer. The fromJson and the toJson handle the conversions.

The null-safety problem. The JSON has the null, and the Dart’s null safety distinguishes the nullable from the non-nullable. The json['name'] as String throws when the value is the null or the wrong type. The json['name'] as String? is the nullable, and the as String is the non-nullable with the runtime check. The fromJson must handle the missing and the null fields.

The trade-off. The manual mapping is the explicit and the verbose. The code generation is the concise and the hidden. The dart:convert is the built-in and the zero-dependency. The json_serializable is the additional dependency and the build step. The trade-off is between the transparency of the manual mapping and the convenience of the code generation.


a. The jsonEncode and the jsonDecode

The jsonEncode converts the Dart object into the JSON string.

import 'dart:convert';

void main() {
  final data = {
    'name': 'Alice',
    'age': 30,
    'active': true,
    'tags': ['admin', 'user'],
  };

  final json = jsonEncode(data);
  print(json);
  // {"name":"Alice","age":30,"active":true,"tags":["admin","user"]}
}

The jsonEncode accepts the Map, the List, the String, the num, the bool, and the null. The other objects require the toEncodable or the custom toJson method .

The jsonDecode parses the JSON string into the Dart object.

void main() {
  final json = '{"name":"Alice","age":30,"active":true,"tags":["admin","user"]}';
  final dynamic data = jsonDecode(json);

  print(data['name']); // Alice
  print(data['age']);  // 30
  print(data['tags']); // [admin, user]
}

The jsonDecode returns the dynamic, and the developer accesses the fields with the [] operator. The nested objects and the arrays are the Map<String, dynamic> and the List<dynamic> .

The Map<String, dynamic> cast. The jsonDecode returns the Map<String, dynamic> for the objects, and the developer can cast the result.

final Map<String, dynamic> data = jsonDecode(json) as Map<String, dynamic>;

The explicit cast makes the type the Map<String, dynamic>, and the field access is the typed.

The JsonEncoder and the JsonDecoder. The JsonEncoder and the JsonDecoder are the classes behind the functions, and they provide the configuration.

const encoder = JsonEncoder.withIndent('  ');
final pretty = encoder.convert(data);
print(pretty);
// {
//   "name": "Alice",
//   "age": 30
// }

The withIndent produces the pretty-printed JSON, and the default is the compact. The JsonDecoder has the reviver for the custom parsing .

The toEncodable. The jsonEncode has the toEncodable parameter for the custom objects:

final json = jsonEncode(customObject, toEncodable: (obj) {
  if (obj is User) {
    return obj.toJson();
  }
  return obj.toJson();
});

The toEncodable is the fallback for the objects the encoder does not know how to serialize.


b. The fromJson and the toJson

The typed class provides the fromJson factory and the toJson method. The fromJson constructs the object from the map, and the toJson returns the map.

class User {
  final int id;
  final String name;
  final String email;
  final bool active;

  User({
    required this.id,
    required this.name,
    required this.email,
    required this.active,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
      active: json['active'] as bool,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'active': active,
    };
  }
}

The factory constructor is the convention for the fromJson, and the toJson returns the Map<String, dynamic>. The as casts are the runtime checks, and the wrong type throws the TypeError .

The nullable fields. The optional fields are the nullable, and the fromJson handles the null:

class User {
  final int id;
  final String name;
  final String? phone;

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      phone: json['phone'] as String?,
    );
  }
}

The as String? is the nullable cast, and the missing key returns the null.

The default values. The fromJson can provide the defaults for the missing fields:

factory User.fromJson(Map<String, dynamic> json) {
  return User(
    id: json['id'] as int,
    name: json['name'] as String,
    active: json['active'] as bool? ?? true,
  );
}

The ?? operator provides the default when the value is the null.

The nested objects. The nested object is the Map<String, dynamic>, and the fromJson calls the nested fromJson:

class Post {
  final int id;
  final String title;
  final User author;

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'] as int,
      title: json['title'] as String,
      author: User.fromJson(json['author'] as Map<String, dynamic>),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'author': author.toJson(),
    };
  }
}

The json['author'] as Map<String, dynamic> casts the nested object, and the User.fromJson constructs the nested class.

The lists. The list of the objects is the List<dynamic>, and the fromJson maps each element:

class UserList {
  final List<User> users;

  factory UserList.fromJson(List<dynamic> json) {
    return UserList(
      users: json
          .map((item) => User.fromJson(item as Map<String, dynamic>))
          .toList(),
    );
  }

  List<Map<String, dynamic>> toJson() {
    return users.map((user) => user.toJson()).toList();
  }
}

The .map transforms each dynamic element into the User, and the .toList() collects the result.

The enums. The enum is the string or the integer, and the fromJson maps it:

enum Role { admin, user, guest }

class User {
  final Role role;

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      role: Role.values.byName(json['role'] as String),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'role': role.name,
    };
  }
}

The Role.values.byName finds the enum value by the name, and the role.name returns the name. The integer convention uses the Role.values[json['role'] as int] .

The DateTime. The DateTime is the ISO 8601 string, and the fromJson parses it:

class Event {
  final DateTime start;
  final DateTime? end;

  factory Event.fromJson(Map<String, dynamic> json) {
    return Event(
      start: DateTime.parse(json['start'] as String),
      end: json['end'] != null
          ? DateTime.parse(json['end'] as String)
          : null,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'start': start.toIso8601String(),
      'end': end?.toIso8601String(),
    };
  }
}

The DateTime.parse parses the ISO 8601 string, and the toIso8601String produces it. The Unix timestamp convention uses the DateTime.fromMillisecondsSinceEpoch and the millisecondsSinceEpoch.


c. The errors and the patterns

The FormatException. The jsonDecode throws the FormatException when the string is not the valid JSON:

try {
  final data = jsonDecode('{invalid json}');
} on FormatException catch (e) {
  print('Invalid JSON: ${e.message}');
}

The FormatException carries the message, the source, and the offset, and the catch handles the malformed JSON.

The TypeError. The as cast throws the TypeError when the value is the wrong type:

try {
  final user = User.fromJson(json);
} on TypeError catch (e) {
  print('Type error: $e');
}

The TypeError is the runtime error, and the fromJson must handle the missing and the wrong-typed fields.

The defensive fromJson. The robust fromJson handles the null and the type with the helpers:

factory User.fromJson(Map<String, dynamic> json) {
  return User(
    id: _parseInt(json['id']),
    name: json['name'] as String? ?? '',
    active: json['active'] as bool? ?? false,
  );
}

static int _parseInt(dynamic value) {
  if (value is int) return value;
  if (value is String) return int.parse(value);
  if (value is double) return value.toInt();
  throw FormatException('Expected an int, got ${value.runtimeType}');
}

The _parseInt handles the int, the string, and the double, and the FormatException is the clear error. The defensive parsing is the robust, and it handles the API that returns the inconsistent types.

The toJson with the null. The toJson omits the null fields or includes them:

Map<String, dynamic> toJson() {
  return {
    'id': id,
    'name': name,
    if (phone != null) 'phone': phone,
  };
}

The if (phone != null) includes the phone only when it is the not-null. The pattern is the clean JSON that omits the optional fields.

The list of the primitives. The list of the strings or the ints is the List<dynamic>, and the cast is the List<String>:

final tags = (json['tags'] as List<dynamic>).cast<String>();

The .cast<String>() is the lazy cast, and the List<String>.from(json['tags']) is the eager.

The json_serializable alternative. The json_serializable package generates the fromJson and the toJson from the annotations:

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  final int id;
  final String name;

  User({required this.id, required this.name});

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

The build runner generates the user.g.dart with the _$UserFromJson and the _$UserToJson. The annotations handle the field names, the defaults, and the custom converters.


Complete Example Session

// ============================================
// PART 1: THE BASIC ENCODE
// ============================================

import 'dart:convert';

void main() {
  final data = {
    'name': 'Alice',
    'age': 30,
    'active': true,
    'tags': ['admin', 'user'],
  };

  final json = jsonEncode(data);
  print(json);
  // {"name":"Alice","age":30,"active":true,"tags":["admin","user"]}
}


// ============================================
// PART 2: THE BASIC DECODE
// ============================================

void main() {
  final json = '{"name":"Alice","age":30,"active":true}';
  final dynamic data = jsonDecode(json);

  print(data['name']); // Alice
  print(data['age']);  // 30
}


// ============================================
// PART 3: THE TYPED CLASS
// ============================================

class User {
  final int id;
  final String name;
  final String email;
  final bool active;

  User({
    required this.id,
    required this.name,
    required this.email,
    required this.active,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
      active: json['active'] as bool,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'active': active,
    };
  }
}


// ============================================
// PART 4: THE NULLABLE AND THE DEFAULT
// ============================================

class User {
  final int id;
  final String name;
  final String? phone;
  final bool active;

  User({
    required this.id,
    required this.name,
    this.phone,
    this.active = true,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      phone: json['phone'] as String?,
      active: json['active'] as bool? ?? true,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      if (phone != null) 'phone': phone,
      'active': active,
    };
  }
}


// ============================================
// PART 5: THE NESTED OBJECT
// ============================================

class Post {
  final int id;
  final String title;
  final User author;

  Post({required this.id, required this.title, required this.author});

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'] as int,
      title: json['title'] as String,
      author: User.fromJson(json['author'] as Map<String, dynamic>),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'author': author.toJson(),
    };
  }
}


// ============================================
// PART 6: THE LIST OF THE OBJECTS
// ============================================

List<User> usersFromJson(List<dynamic> json) {
  return json
      .map((item) => User.fromJson(item as Map<String, dynamic>))
      .toList();
}

List<Map<String, dynamic>> usersToJson(List<User> users) {
  return users.map((user) => user.toJson()).toList();
}


// ============================================
// PART 7: THE ENUM
// ============================================

enum Role { admin, user, guest }

class Account {
  final Role role;

  Account({required this.role});

  factory Account.fromJson(Map<String, dynamic> json) {
    return Account(
      role: Role.values.byName(json['role'] as String),
    );
  }

  Map<String, dynamic> toJson() {
    return {'role': role.name};
  }
}


// ============================================
// PART 8: THE DATETIME
// ============================================

class Event {
  final String name;
  final DateTime start;
  final DateTime? end;

  Event({required this.name, required this.start, this.end});

  factory Event.fromJson(Map<String, dynamic> json) {
    return Event(
      name: json['name'] as String,
      start: DateTime.parse(json['start'] as String),
      end: json['end'] != null
          ? DateTime.parse(json['end'] as String)
          : null,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'start': start.toIso8601String(),
      'end': end?.toIso8601String(),
    };
  }
}


// ============================================
// PART 9: THE ERROR HANDLING
// ============================================

void parseJson(String source) {
  try {
    final data = jsonDecode(source);
    final user = User.fromJson(data as Map<String, dynamic>);
    print('Parsed: ${user.name}');
  } on FormatException catch (e) {
    print('Invalid JSON: ${e.message}');
  } on TypeError catch (e) {
    print('Type error: $e');
  } catch (e) {
    print('Unknown error: $e');
  }
}


// ============================================
// PART 10: THE COMPLETE EXAMPLE
// ============================================

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) {
    return 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,
  };
}

class Customer {
  final int id;
  final String name;
  final String email;
  final Address? address;
  final List<String> tags;
  final DateTime createdAt;

  Customer({
    required this.id,
    required this.name,
    required this.email,
    this.address,
    required this.tags,
    required this.createdAt,
  });

  factory Customer.fromJson(Map<String, dynamic> json) {
    return Customer(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
      address: json['address'] != null
          ? Address.fromJson(json['address'] as Map<String, dynamic>)
          : null,
      tags: (json['tags'] as List<dynamic>).cast<String>(),
      createdAt: DateTime.parse(json['createdAt'] as String),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      if (address != null) 'address': address!.toJson(),
      'tags': tags,
      'createdAt': createdAt.toIso8601String(),
    };
  }
}

void main() {
  final json = '''
  {
    "id": 1,
    "name": "Alice",
    "email": "alice@example.com",
    "address": {
      "street": "123 Main St",
      "city": "Springfield",
      "zip": "12345"
    },
    "tags": ["vip", "wholesale"],
    "createdAt": "2024-01-15T10:30:00.000Z"
  }
  ''';

  final customer = Customer.fromJson(
    jsonDecode(json) as Map<String, dynamic>,
  );

  print(customer.name);          // Alice
  print(customer.address?.city); // Springfield
  print(customer.tags);          // [vip, wholesale]
  print(customer.createdAt);     // 2024-01-15 10:30:00.000Z

  final encoded = jsonEncode(customer.toJson());
  print(encoded);
}

The ten parts show the basic encode, the basic decode, the typed class, the nullable and the default, the nested object, the list of the objects, the enum, the DateTime, the error handling, and the complete example.


Quick Reference

The Core Functions

FunctionThe purpose
jsonEncode(object)The Dart object to the JSON string
jsonDecode(string)The JSON string to the Dart object
JsonEncoder().convert(obj)The configurable encoder
JsonEncoder.withIndent(' ')The pretty-printed encoder
JsonDecoder().convert(str)The configurable decoder

The JSON Types to the Dart Types

The JSONThe Dart
The objectThe Map<String, dynamic>
The arrayThe List<dynamic>
The stringThe String
The number (int)The int
The number (double)The double
The booleanThe bool
The nullThe null

The fromJson Patterns

The patternThe example
The required fieldjson['id'] as int
The nullable fieldjson['phone'] as String?
The defaultjson['active'] as bool? ?? true
The nested objectUser.fromJson(json['author'] as Map<String, dynamic>)
The list(json['tags'] as List<dynamic>).cast<String>()
The list of the objectsjson.map((e) => User.fromJson(e)).toList()
The enumRole.values.byName(json['role'] as String)
The DateTimeDateTime.parse(json['date'] as String)

The toJson Patterns

The patternThe example
The simple field'id': id
The optional fieldif (phone != null) 'phone': phone
The nested object'author': author.toJson()
The list'tags': tags
The enum'role': role.name
The DateTime'date': date.toIso8601String()

The Errors

The errorThe cause
FormatExceptionThe malformed JSON
TypeErrorThe wrong type in the cast
NoSuchMethodErrorThe missing field access
The null checkThe missing required field

Best Practices

✅ Do This:

// Use the fromJson factory
factory User.fromJson(Map<String, dynamic> json) => User(...);            // ✅
// Use the toJson method
Map<String, dynamic> toJson() => {'id': id, 'name': name};                // ✅
// Handle the nullable with the ?
phone: json['phone'] as String?,                                          // ✅
// Provide the defaults with the ??
active: json['active'] as bool? ?? true,                                  // ✅
// Cast the list
(json['tags'] as List<dynamic>).cast<String>(),                           // ✅
// Parse the DateTime
DateTime.parse(json['date'] as String),                                   // ✅
// Map the enum
Role.values.byName(json['role'] as String),                               // ✅
// Catch the FormatException
try { jsonDecode(s); } on FormatException catch (e) { ... }               // ✅

❌ Don’t Do This:

// Don't use the dynamic without the cast
final user = jsonDecode(s); user.name;  // the runtime error               // ❌
// Don't forget the null check
final phone = json['phone'] as String;  // the throws on null              // ❌
// Don't use the string concatenation for the JSON
final json = '{"name":"' + name + '"}';  // the injection                  // ❌
// Don't forget the Content-Type for the HTTP
// The POST with the JSON body needs the header                            // ❌
// Don't use the DateTime directly
DateTime.parse(json['date']);  // the missing cast                         // ❌
// Don't ignore the FormatException
jsonDecode(invalid);  // the unhandled                                     // ❌
// Don't forget the toList()
json.map((e) => User.fromJson(e));  // the lazy Iterable                   // ❌

Common Pitfalls

The pitfallThe whyThe fix
The TypeErrorThe wrong castCheck the type
The null errorThe missing fieldUse the ? and the ??
The FormatExceptionThe malformed JSONCatch and handle
The list is the IterableThe missing .toList()Add the .toList()
The DateTime is the stringThe missing parseUse DateTime.parse
The enum is the stringThe missing lookupUse Role.values.byName
The nested object is the mapThe missing fromJsonCall the nested fromJson
The number is the int or the doubleThe JSON has one typeCheck the runtime type

Real-World Examples

1. The Basic Encode

final json = jsonEncode({'name': 'Alice', 'age': 30});

2. The Basic Decode

final data = jsonDecode('{"name":"Alice"}');

3. The Typed Class

factory User.fromJson(Map<String, dynamic> json) => User(id: json['id'] as int);

4. The Nullable

phone: json['phone'] as String?,

5. The Default

active: json['active'] as bool? ?? true,

6. The Nested

author: User.fromJson(json['author'] as Map<String, dynamic>),

7. The List

tags: (json['tags'] as List<dynamic>).cast<String>(),

8. The Enum

role: Role.values.byName(json['role'] as String),

9. The DateTime

start: DateTime.parse(json['start'] as String),

10. The Complete

final customer = Customer.fromJson(jsonDecode(json));
final encoded = jsonEncode(customer.toJson());

Visual

The JSON-to-Dart Mapping

┌──────────────────────────────────────────────────────────────┐
│  THE JSON-TO-DART MAPPING                                    │
│                                                              │
│  THE JSON:                                                   │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  {                                                   │    │
│  │    "id": 1,                                          │    │
│  │    "name": "Alice",                                  │    │
│  │    "active": true,                                   │    │
│  │    "tags": ["vip", "wholesale"],                     │    │
│  │    "address": { "city": "Springfield" },             │    │
│  │    "createdAt": "2024-01-15T10:30:00.000Z"           │    │
│  │  }                                                   │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  jsonDecode                                             │
│    ▼                                                         │
│  THE DART DYNAMIC:                                           │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  Map<String, dynamic> {                              │    │
│  │    'id': int 1,                                      │    │
│  │    'name': String 'Alice',                           │    │
│  │    'active': bool true,                              │    │
│  │    'tags': List<dynamic> ['vip', 'wholesale'],       │    │
│  │    'address': Map<String, dynamic> { ... },          │    │
│  │    'createdAt': String '2024-01-15T10:30:00.000Z',   │    │
│  │  }                                                   │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  Customer.fromJson                                      │
│    ▼                                                         │
│  THE DART TYPED:                                             │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  Customer {                                          │    │
│  │    id: 1,                                            │    │
│  │    name: 'Alice',                                    │    │
│  │    active: true,                                     │    │
│  │    tags: ['vip', 'wholesale'],                       │    │
│  │    address: Address { city: 'Springfield' },         │    │
│  │    createdAt: DateTime(2024, 1, 15),                 │    │
│  │  }                                                   │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Encode and the Decode

┌──────────────────────────────────────────────────────────────┐
│  THE ENCODE AND THE DECODE                                   │
│                                                              │
│  THE DART OBJECT:                                            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  User(id: 1, name: 'Alice', active: true)            │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  toJson()                                               │
│    ▼                                                         │
│  THE MAP:                                                    │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  {'id': 1, 'name': 'Alice', 'active': true}          │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  jsonEncode                                             │
│    ▼                                                         │
│  THE JSON STRING:                                            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  '{"id":1,"name":"Alice","active":true}'             │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  jsonDecode                                             │
│    ▼                                                         │
│  THE MAP:                                                    │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  {'id': 1, 'name': 'Alice', 'active': true}          │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  fromJson()                                             │
│    ▼                                                         │
│  THE DART OBJECT:                                            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  User(id: 1, name: 'Alice', active: true)            │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Error Handling

┌──────────────────────────────────────────────────────────────┐
│  THE ERROR HANDLING                                          │
│                                                              │
│  try {                                                       │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  final data = jsonDecode(source);                    │    │
│  │  final user = User.fromJson(data);                   │    │
│  └──────────────────────────────────────────────────────┘    │
│  } on FormatException catch (e) {                            │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The malformed JSON.                                 │    │
│  │  e.message, e.source, e.offset                       │    │
│  └──────────────────────────────────────────────────────┘    │
│  } on TypeError catch (e) {                                  │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The wrong type in the cast.                         │    │
│  │  'Expected a value of type int, but got String'      │    │
│  └──────────────────────────────────────────────────────┘    │
│  } catch (e) {                                               │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The unknown error.                                  │    │
│  └──────────────────────────────────────────────────────┘    │
│  }                                                           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The json_serializable Alternative

┌──────────────────────────────────────────────────────────────┐
│  THE json_serializable ALTERNATIVE                           │
│                                                              │
│  @JsonSerializable()                                         │
│  class User {                                                │
│    final int id;                                             │
│    final String name;                                        │
│                                                              │
│    User({required this.id, required this.name});             │
│                                                              │
│    factory User.fromJson(Map<String, dynamic> json) =>       │
│        _$UserFromJson(json);                                 │
│                                                              │
│    Map<String, dynamic> toJson() => _$UserToJson(this);      │
│  }                                                           │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The build_runner generates the user.g.dart:         │    │
│  │                                                       │    │
│  │  User _$UserFromJson(Map<String, dynamic> json) {    │    │
│  │    return User(                                      │    │
│  │      id: json['id'] as int,                          │    │
│  │      name: json['name'] as String,                   │    │
│  │    );                                                │    │
│  │  }                                                   │    │
│  │                                                       │    │
│  │  Map<String, dynamic> _$UserToJson(User instance) {  │    │
│  │    return {                                          │    │
│  │      'id': instance.id,                              │    │
│  │      'name': instance.name,                          │    │
│  │    };                                                │    │
│  │  }                                                   │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The annotations are the source; the generated code is the   │
│  boilerplate.                                                │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
jsonEncodeThe Dart object to the JSON string
jsonDecodeThe JSON string to the Dart object
The decoded typesThe Map, the List, the String, the num, the bool, the null
fromJsonThe factory that constructs the typed object
toJsonThe method that returns the map
The nullableThe as String?
The defaultThe as bool? ?? true
The nestedThe User.fromJson(json['author'])
The listThe .map().toList()
The enumThe Role.values.byName
The DateTimeThe DateTime.parse
The errorThe FormatException

Key takeaways:

  • The dart:convert library provides the jsonEncode and the jsonDecode. The jsonEncode converts the Dart object into the JSON string, and the jsonDecode parses the JSON string into the Dart object. The library is the built-in codec, and the two functions are the foundation .
  • The jsonDecode returns the dynamic. The Map<String, dynamic> is the object, the List<dynamic> is the array, and the primitives are the String, the num, the bool, and the null. The typed class with the fromJson factory restores the type safety .
  • The fromJson factory and the toJson method are the mapping. The fromJson constructs the typed object from the map, and the toJson returns the map. The as casts are the runtime checks, and the wrong type throws the TypeError .
  • The nullable and the default handle the optional fields. The as String? is the nullable cast, and the as bool? ?? true provides the default. The if (phone != null) in the toJson omits the null fields .
  • The nested objects and the lists are the composition. The User.fromJson(json['author'] as Map<String, dynamic>) handles the nested object, and the .map((e) => User.fromJson(e)).toList() handles the list of the objects .
  • The enum and the DateTime need the conversion. The Role.values.byName(json['role'] as String) maps the enum, and the DateTime.parse(json['date'] as String) parses the DateTime. The toIso8601String produces the DateTime string .
  • The FormatException is the malformed JSON, and the TypeError is the wrong type. The try/catch handles both, and the defensive fromJson handles the inconsistent types with the helper functions .
  • The json_serializable is the code-generation alternative. The package generates the fromJson and the toJson from the annotations, and the build runner produces the .g.dart file. The manual approach is the foundation, and the code generation is the convenience .

Remember: The dart:convert library is the built-in JSON codec. The jsonEncode converts the Dart object into the JSON string, and the jsonDecode parses the JSON string into the Dart object. The jsonDecode returns the dynamic, and the typed class with the fromJson factory and the toJson method restores the type safety. The nullable, the default, the nested objects, the lists, the enums, and the DateTime are the patterns. The FormatException and the TypeError are the errors. The result is the reliable JSON serialization, which is the foundation of the data interchange.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!