1. JSON Serialization / Deserialization là gì?
Trong quá trình phát triển ứng dụng, đặc biệt là ứng dụng Flutter có kết nối với Backend, việc trao đổi dữ liệu giữa các thành phần là một công việc rất quan trọng. Ví dụ, khi người dùng mở ứng dụng bán sữa, ứng dụng cần lấy danh sách sản phẩm từ máy chủ để hiển thị tên sản phẩm, giá bán, hình ảnh, mô tả và thông tin tồn kho. Khi người dùng đặt hàng, ứng dụng lại cần gửi thông tin đơn hàng lên máy chủ để Backend xử lý.
Tuy nhiên, dữ liệu trong chương trình Flutter thường được tổ chức thành các biến, đối tượng (object) hoặc lớp (class) của Dart. Trong khi đó, dữ liệu được truyền qua HTTP API thường được biểu diễn dưới dạng JSON (JavaScript Object Notation). Hai cách biểu diễn này không hoàn toàn giống nhau. Vì vậy, ứng dụng cần có cơ chế chuyển đổi dữ liệu giữa đối tượng Dart và định dạng JSON.
Đây chính là lúc JSON Serialization và JSON Deserialization được sử dụng.
- Serialization (tuần tự hóa): Là quá trình chuyển đổi dữ liệu hoặc đối tượng trong chương trình thành một định dạng có thể lưu trữ hoặc truyền đi, chẳng hạn như JSON.
- Deserialization (giải tuần tự hóa): Là quá trình đọc dữ liệu đã được tuần tự hóa và chuyển đổi nó trở lại thành dữ liệu hoặc đối tượng mà chương trình có thể sử dụng.
Trong Flutter, có thể hiểu đơn giản rằng Serialization thường được dùng khi ứng dụng muốn chuyển một đối tượng Dart thành JSON để gửi lên Backend hoặc lưu trữ; còn Deserialization được dùng khi ứng dụng nhận JSON từ Backend rồi chuyển thành đối tượng Dart để xử lý và hiển thị trên giao diện.
Ví dụ, ứng dụng có một đối tượng sản phẩm trong Dart:
class Product {
final int id;
final String name;
final double price;
Product({
required this.id,
required this.name,
required this.price,
});
}
Một đối tượng sản phẩm có thể được khởi tạo như sau:
final product = Product(
id: 1,
name: 'Sữa dinh dưỡng',
price: 250000,
);
Trong chương trình Dart, dữ liệu này được tổ chức thành một đối tượng Product. Nhưng khi cần gửi dữ liệu qua HTTP API, ứng dụng có thể chuyển nó thành JSON:
{
"id": 1,
"name": "Sữa dinh dưỡng",
"price": 250000
}
Quá trình chuyển đối tượng Product thành dữ liệu JSON được gọi là Serialization. Ngược lại, khi ứng dụng nhận được JSON có nội dung tương tự và chuyển nó thành đối tượng Product, quá trình đó được gọi là Deserialization.
Cần lưu ý rằng JSON chỉ là một định dạng biểu diễn dữ liệu, không phải một đối tượng Dart. Sau khi nhận JSON, chương trình thường cần phân tích dữ liệu rồi mới tạo ra đối tượng tương ứng.
2. JSON là gì và tại sao ứng dụng cần JSON?
JSON là viết tắt của JavaScript Object Notation, một định dạng dữ liệu dạng văn bản được thiết kế để biểu diễn và trao đổi dữ liệu có cấu trúc. Mặc dù tên gọi có nhắc đến JavaScript, JSON không chỉ được sử dụng trong JavaScript mà còn được hỗ trợ rộng rãi bởi Dart, Java, PHP, Python, C#, JavaScript và nhiều ngôn ngữ lập trình khác.
Một dữ liệu JSON thường bao gồm các cặp khóa và giá trị (key-value), mảng (array), hoặc những cấu trúc lồng nhau.
Ví dụ:
{
"id": 10,
"name": "Sữa bột trẻ em",
"price": 320000,
"inStock": true,
"description": null,
"categories": ["Sữa trẻ em", "Dinh dưỡng"],
"brand": {
"id": 3,
"name": "Thương hiệu A"
}
}
Trong ví dụ trên:
idcó giá trị là10, thuộc kiểu số nguyên.namecó giá trị là chuỗi ký tự.pricelà giá trị số.inStocklà giá trị Boolean, chỉ nhậntruehoặcfalse.descriptioncó giá trịnull, nghĩa là hiện không có giá trị được cung cấp.categorieslà một mảng chứa nhiều chuỗi.brandlà một đối tượng JSON lồng bên trong đối tượng sản phẩm.
JSON hỗ trợ những kiểu dữ liệu cơ bản như chuỗi, số, Boolean, null, object và array. JSON không trực tiếp biểu diễn các kiểu đặc thù của Dart như DateTime, DateTime cần được chuyển thành chuỗi hoặc một giá trị phù hợp trước khi truyền đi.
JSON được sử dụng phổ biến vì có cấu trúc tương đối dễ đọc, dễ kiểm tra và được nhiều ngôn ngữ lập trình hỗ trợ. Khi Frontend và Backend được xây dựng bằng hai ngôn ngữ khác nhau, JSON cung cấp một định dạng chung để cả hai bên có thể trao đổi dữ liệu.
Ví dụ, Frontend có thể được xây dựng bằng Flutter và Dart, trong khi Backend được xây dựng bằng Laravel và PHP. Hai bên không cần sử dụng chung một ngôn ngữ lập trình. Chỉ cần thống nhất cấu trúc dữ liệu API, Flutter có thể gửi và nhận JSON do Laravel xử lý.
3. Tại sao không sử dụng trực tiếp đối tượng Dart để gửi lên Backend?
Đây là một vấn đề quan trọng để hiểu rõ bản chất của Serialization và Deserialization.
Một đối tượng Dart tồn tại trong bộ nhớ của chương trình khi ứng dụng đang chạy. Đối tượng đó có thể chứa các thuộc tính, phương thức và những thông tin gắn với cách Dart quản lý dữ liệu. Tuy nhiên, khi gửi một HTTP Request qua mạng, ứng dụng cần chuyển nội dung thành các byte để truyền đi.
Backend không thể tự động truy cập vào đối tượng đang nằm trong bộ nhớ của ứng dụng Flutter. Máy chủ chỉ nhận được dữ liệu mà ứng dụng thực sự gửi qua yêu cầu HTTP.
Vì vậy, ứng dụng thường thực hiện quy trình sau:
- Tạo hoặc lấy dữ liệu từ đối tượng Dart.
- Chuyển dữ liệu thành cấu trúc JSON.
- Mã hóa JSON thành chuỗi hoặc byte để gửi qua HTTP.
- Backend nhận yêu cầu, đọc JSON và chuyển dữ liệu thành cấu trúc mà Backend có thể xử lý.
- Backend thực hiện nghiệp vụ, truy vấn hoặc cập nhật cơ sở dữ liệu.
- Backend tạo phản hồi JSON và gửi về ứng dụng.
- Flutter đọc JSON, chuyển thành đối tượng Dart và sử dụng để cập nhật giao diện.
Có thể hình dung luồng dữ liệu như sau:
Đối tượng Dart → Serialization → JSON → HTTP Request → Backend
Khi nhận dữ liệu từ máy chủ:
HTTP Response → JSON → Deserialization → Đối tượng Dart → Giao diện Flutter
Đây là cách hai bên trao đổi dữ liệu mà không phụ thuộc vào việc chúng được viết bằng cùng một ngôn ngữ hay không.
4. Serialization trong Dart hoạt động như thế nào?
Trong Dart, cách cơ bản để thực hiện Serialization là viết một phương thức chuyển đối tượng thành Map<String, dynamic>, sau đó dùng jsonEncode() để chuyển Map thành chuỗi JSON.
Map<String, dynamic> là một cấu trúc lưu trữ dữ liệu theo cặp khóa và giá trị. Trong đó, String thể hiện kiểu dữ liệu của khóa, còn dynamic cho phép giá trị có thể thuộc nhiều kiểu dữ liệu khác nhau.
Ví dụ, ta có lớp Product:
class Product {
final int id;
final String name;
final double price;
Product({
required this.id,
required this.name,
required this.price,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'price': price,
};
}
}
Trong lớp này, phương thức toJson() chịu trách nhiệm lấy các thuộc tính của đối tượng rồi đưa chúng vào một Map.
Giả sử tạo một sản phẩm:
final product = Product(
id: 1,
name: 'Sữa dinh dưỡng',
price: 250000,
);
Khi gọi:
final data = product.toJson();
print(data);
Kết quả là một Map có dữ liệu tương đương:
{
'id': 1,
'name': 'Sữa dinh dưỡng',
'price': 250000.0
}
Ở bước này, dữ liệu đã được chuyển từ đối tượng Product sang Map, nhưng Map vẫn là cấu trúc dữ liệu của Dart, chưa phải chuỗi JSON.
Để chuyển Map thành chuỗi JSON, ta sử dụng jsonEncode() thuộc thư viện dart:convert.
import 'dart:convert';
final jsonString = jsonEncode(product.toJson());
print(jsonString);
Kết quả:
{"id":1,"name":"Sữa dinh dưỡng","price":250000.0}
Như vậy, có hai bước cần phân biệt:
toJson()chuyển đối tượng Dart thành Map.jsonEncode()chuyển Map hoặc dữ liệu tương thích thành chuỗi JSON.
Trong nhiều dự án, tên toJson() được sử dụng như một quy ước phổ biến để thể hiện rằng đối tượng có khả năng chuyển đổi sang dạng JSON. Đây không phải từ khóa đặc biệt của Dart; lập trình viên tự định nghĩa phương thức này.
Ngoài ra, jsonEncode() không tự động hiểu mọi đối tượng tùy chỉnh của Dart. Nếu truyền một đối tượng không được hỗ trợ trực tiếp, chương trình có thể báo lỗi. Vì vậy, ta thường chuyển đối tượng thành Map trước khi gọi jsonEncode().
5. Deserialization trong Dart hoạt động như thế nào?
Nếu Serialization chuyển dữ liệu từ đối tượng Dart sang JSON, thì Deserialization thực hiện chiều ngược lại: đọc JSON và tạo ra đối tượng Dart.
Giả sử Backend trả về dữ liệu sản phẩm:
{
"id": 1,
"name": "Sữa dinh dưỡng",
"price": 250000
}
Khi Flutter nhận phản hồi HTTP, phần nội dung thường được truy cập dưới dạng chuỗi hoặc byte. Trước tiên, ứng dụng có thể sử dụng jsonDecode() để phân tích chuỗi JSON thành cấu trúc dữ liệu Dart.
import 'dart:convert';
const jsonString = '''
{
"id": 1,
"name": "Sữa dinh dưỡng",
"price": 250000
}
''';
final data = jsonDecode(jsonString);
print(data);
Với một JSON object ở cấp cao nhất, kết quả thường là Map<String, dynamic> về mặt cấu trúc dữ liệu. Tuy nhiên, kiểu tĩnh mà Dart suy ra hoặc công cụ hiển thị có thể là dynamic, do jsonDecode() trả về dynamic.
Điều này có nghĩa là dữ liệu đã được phân tích thành Map, nhưng ứng dụng chưa nhất thiết có một đối tượng thuộc lớp Product.
Để tạo đối tượng Product, ta bổ sung phương thức fromJson():
class Product {
final int id;
final String name;
final double price;
Product({
required this.id,
required this.name,
required this.price,
});
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'] as int,
name: json['name'] as String,
price: (json['price'] as num).toDouble(),
);
}
}
Phương thức fromJson() nhận dữ liệu dạng Map và tạo ra một đối tượng Product.
Ví dụ sử dụng:
final Map<String, dynamic> data =
jsonDecode(jsonString) as Map<String, dynamic>;
final product = Product.fromJson(data);
print(product.name);
print(product.price);
Kết quả:
Sữa dinh dưỡng
250000.0
Trong đoạn code này:
jsonDecode(jsonString)phân tích chuỗi JSON.as Map<String, dynamic>xác nhận rằng dữ liệu được sử dụng như một Map có khóa kiểu chuỗi.Product.fromJson(data)tạo đối tượngProduct.product.namevàproduct.pricetruy cập trực tiếp vào thuộc tính của đối tượng.
Từ đây, lập trình viên có thể sử dụng dữ liệu theo cấu trúc rõ ràng thay vì phải truy cập từng khóa JSON ở nhiều nơi trong ứng dụng.
Cần phân biệt rằng jsonDecode() chỉ phân tích dữ liệu JSON thành các cấu trúc Dart cơ bản. Nó không tự động tạo ra lớp Product chỉ vì dữ liệu có những khóa id, name và price. Việc ánh xạ dữ liệu JSON vào đúng lớp thường được thực hiện thông qua fromJson() hoặc một công cụ sinh code.
6. Phân biệt jsonEncode(), jsonDecode(), toJson() và fromJson()
Bốn thành phần này thường xuất hiện cùng nhau trong quá trình xử lý JSON, nhưng nhiệm vụ của chúng khác nhau.
toJson() là phương thức thường được lập trình viên định nghĩa trong một lớp để chuyển đối tượng thành Map.
jsonEncode() là hàm của thư viện dart:convert, dùng để chuyển dữ liệu tương thích như Map hoặc List thành chuỗi JSON.
jsonDecode() là hàm dùng để phân tích chuỗi JSON thành các cấu trúc dữ liệu Dart như Map, List, String, số, Boolean hoặc null, tùy theo dữ liệu đầu vào.
fromJson() là phương thức thường được định nghĩa để chuyển Map JSON thành một đối tượng cụ thể.
Có thể tóm tắt bằng luồng chuyển đổi sau:
Đối tượng Dart → toJson() → Map → jsonEncode() → Chuỗi JSON.
Chuỗi JSON → jsonDecode() → Map → fromJson() → Đối tượng Dart.
Không phải dự án nào cũng bắt buộc phải sử dụng đủ bốn thành phần trong mọi tình huống. Ví dụ, nếu chỉ cần đọc một giá trị đơn giản từ JSON, ứng dụng có thể sử dụng trực tiếp kết quả của jsonDecode(). Tuy nhiên, khi xử lý dữ liệu nghiệp vụ như sản phẩm, tài khoản hoặc đơn hàng, việc xây dựng các lớp dữ liệu và phương thức chuyển đổi thường giúp chương trình dễ đọc, dễ bảo trì hơn.
7. Ví dụ hoàn chỉnh: Serialization và Deserialization cho sản phẩm
Để hiểu rõ hơn, ta xây dựng một ví dụ tương đối đầy đủ với lớp Product.
import 'dart:convert';
class Product {
final int id;
final String name;
final double price;
final bool inStock;
Product({
required this.id,
required this.name,
required this.price,
required this.inStock,
});
// Chuyển đối tượng Product thành Map
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'price': price,
'inStock': inStock,
};
}
// Tạo đối tượng Product từ Map
factory Product.fromJson(Map<String, dynamic> json) {
return Product(
id: json['id'] as int,
name: json['name'] as String,
price: (json['price'] as num).toDouble(),
inStock: json['inStock'] as bool,
);
}
}
void main() {
// 1. Tạo đối tượng Dart
final product = Product(
id: 1,
name: 'Sữa dinh dưỡng',
price: 250000,
inStock: true,
);
// 2. Serialization: đối tượng -> chuỗi JSON
final jsonString = jsonEncode(product.toJson());
print('JSON gửi đi: $jsonString');
// 3. Deserialization: chuỗi JSON -> đối tượng Dart
final decoded = jsonDecode(jsonString)
as Map<String, dynamic>;
final restoredProduct = Product.fromJson(decoded);
print('Tên sản phẩm: ${restoredProduct.name}');
print('Giá bán: ${restoredProduct.price}');
print('Còn hàng: ${restoredProduct.inStock}');
}
Đoạn code này minh họa đầy đủ cả hai chiều chuyển đổi.
Đầu tiên, chương trình tạo đối tượng Product với bốn thuộc tính. Tiếp theo, toJson() chuyển đối tượng thành Map và jsonEncode() tạo ra chuỗi JSON. Đây là hướng chuyển đổi thường được sử dụng khi cần gửi dữ liệu đi.
Sau đó, chương trình dùng jsonDecode() để phân tích chuỗi JSON thành Map rồi gọi Product.fromJson() để tạo lại đối tượng. Đây là hướng chuyển đổi thường được sử dụng khi nhận dữ liệu từ API.
Trong ví dụ này, chuỗi JSON được tạo và đọc ngay trong cùng một chương trình để minh họa. Trong ứng dụng thực tế, chuỗi JSON thường đến từ phản hồi HTTP của Backend hoặc được gửi đi trong phần body của một HTTP Request.
Một điểm đáng chú ý là price được chuyển đổi thông qua kiểu num rồi gọi .toDouble(). Lý do là dữ liệu số trong JSON có thể được phân tích thành int hoặc double tùy giá trị và cách biểu diễn. Chấp nhận kiểu num rồi chuyển sang double giúp hạn chế lỗi khi giá trị thực tế là số nguyên nhưng lớp Dart yêu cầu double.
8. JSON Serialization / Deserialization được sử dụng ở đâu trong Flutter?
Trong ứng dụng Flutter, hai quá trình này không chỉ phục vụ việc gửi và nhận dữ liệu API. Chúng còn xuất hiện trong nhiều tình huống khác nhau.
8.1. Khi lấy danh sách sản phẩm từ Backend
Giả sử ứng dụng gọi API để lấy danh sách sản phẩm. Backend trả về:
[
{
"id": 1,
"name": "Sữa trẻ em",
"price": 320000,
"inStock": true
},
{
"id": 2,
"name": "Sữa người lớn tuổi",
"price": 280000,
"inStock": true
}
]
Ở đây, dữ liệu cấp cao nhất là một mảng JSON, không phải một JSON object đơn lẻ. Vì vậy, sau khi gọi jsonDecode(), ứng dụng cần xử lý một List và chuyển từng phần tử thành Product.
Ví dụ:
final List<dynamic> decoded =
jsonDecode(jsonString) as List<dynamic>;
final products = decoded
.map(
(item) => Product.fromJson(
item as Map<String, dynamic>,
),
)
.toList();
Sau khi chuyển đổi, products là danh sách các đối tượng Product. Giao diện Flutter có thể sử dụng danh sách này để hiển thị bằng ListView.builder hoặc những widget phù hợp khác.
Điều quan trọng là phải xác định đúng cấu trúc dữ liệu mà API trả về. Nếu API trả về một object có chứa khóa data, ứng dụng cần truy cập vào phần data trước khi chuyển đổi danh sách. Không nên mặc định mọi API đều trả về một List trực tiếp.
8.2. Khi gửi thông tin đăng ký hoặc đăng nhập
Khi người dùng nhập email và mật khẩu, Flutter có thể tạo một Map chứa dữ liệu cần gửi:
final requestData = {
'email': email,
'password': password,
};
final body = jsonEncode(requestData);
Chuỗi JSON sau đó được đưa vào body của HTTP Request. Backend nhận dữ liệu, kiểm tra thông tin và xử lý xác thực.
Trong ví dụ này, dữ liệu đầu vào đơn giản nên không nhất thiết phải tạo một lớp riêng. Tuy nhiên, với những request phức tạp hơn như tạo đơn hàng, cập nhật hồ sơ hoặc áp dụng mã giảm giá, việc tạo lớp request model có thể giúp cấu trúc dữ liệu rõ ràng và dễ kiểm soát.
Đối với thông tin đăng nhập, ứng dụng cần gửi dữ liệu qua HTTPS và tránh ghi mật khẩu hoặc token vào log. Serialization chỉ chuyển đổi dữ liệu; nó không mã hóa mật khẩu và không tự làm cho dữ liệu trở nên an toàn.
8.3. Khi nhận thông tin đơn hàng
Một đơn hàng có thể chứa nhiều thông tin: mã đơn, trạng thái, tổng tiền, địa chỉ giao hàng và danh sách sản phẩm. JSON biểu diễn được những dữ liệu lồng nhau này.
Ví dụ:
{
"id": 1001,
"status": "processing",
"total": 570000,
"items": [
{
"productId": 1,
"quantity": 1,
"price": 320000
},
{
"productId": 2,
"quantity": 1,
"price": 250000
}
]
}
Khi xử lý dữ liệu như trên, ứng dụng có thể xây dựng các lớp Order và OrderItem. Order.fromJson() sẽ đọc thông tin đơn hàng, đồng thời chuyển từng phần tử trong items thành đối tượng OrderItem.
Cách làm này giúp chương trình tổ chức dữ liệu theo đúng nghiệp vụ. Thay vì rải rác các thao tác như json['items'][0]['quantity'] ở nhiều nơi, ứng dụng có thể truy cập thông qua các thuộc tính của đối tượng.
Đối với dữ liệu lồng nhau, cần nhớ rằng việc chuyển đổi lớp bên ngoài không tự động chuyển các Map bên trong thành những đối tượng tùy chỉnh. Lập trình viên cần xử lý từng tầng dữ liệu hoặc sử dụng công cụ sinh code để tạo các hàm chuyển đổi phù hợp.
8.4. Khi lưu dữ liệu cục bộ
Một số ứng dụng cần lưu dữ liệu trên thiết bị để có thể truy cập nhanh hoặc sử dụng khi mất kết nối mạng. JSON có thể được dùng làm định dạng trung gian để chuyển đổi đối tượng thành chuỗi trước khi lưu.
Tuy nhiên, JSON không phải cơ sở dữ liệu. Để lưu dữ liệu, ứng dụng vẫn cần một nơi lưu trữ như tệp, SharedPreferences cho dữ liệu đơn giản, hoặc cơ sở dữ liệu cục bộ như Hive hay SQLite tùy yêu cầu.
Ví dụ, đối tượng sản phẩm có thể được chuyển thành JSON để lưu vào bộ nhớ cục bộ. Khi cần đọc lại, ứng dụng lấy chuỗi đã lưu, gọi jsonDecode() rồi tạo lại đối tượng.
Nếu dữ liệu cần truy vấn phức tạp, có quan hệ giữa nhiều bảng hoặc yêu cầu bảo đảm tính nhất quán cao, cần cân nhắc giải pháp lưu trữ phù hợp thay vì chỉ lưu những chuỗi JSON rời rạc.
9. Sử dụng JSON Serialization / Deserialization khi kết nối REST API
Để thấy rõ vai trò của JSON trong một ứng dụng thực tế, hãy xét kiến trúc gồm Flutter, Laravel Backend và MySQL.
Giả sử Flutter cần lấy danh sách sản phẩm từ API:
GET /api/products
Backend Laravel tiếp nhận yêu cầu, truy vấn sản phẩm trong MySQL, rồi trả về dữ liệu theo định dạng JSON. Flutter nhận HTTP Response, đọc phần body và chuyển đổi dữ liệu thành các đối tượng Product.
Luồng xử lý có thể diễn ra như sau:
- Flutter gửi HTTP Request đến endpoint
/api/products. - Laravel tiếp nhận yêu cầu và thực hiện logic nghiệp vụ.
- Laravel truy vấn dữ liệu trong MySQL.
- Laravel tạo HTTP Response chứa JSON.
- Flutter kiểm tra mã trạng thái HTTP và đọc response body.
- Flutter gọi
jsonDecode()để phân tích dữ liệu JSON. - Flutter gọi
Product.fromJson()để tạo đối tượng Dart. - Giao diện sử dụng danh sách đối tượng để hiển thị sản phẩm.
Ví dụ minh họa cách đọc response bằng package http:
import 'dart:convert';
import 'package:http/http.dart' as http;
Future<List<Product>> fetchProducts() async {
final response = await http.get(
Uri.parse('https://example.com/api/products'),
);
if (response.statusCode != 200) {
throw Exception(
'Không thể tải danh sách sản phẩm',
);
}
final List<dynamic> data =
jsonDecode(response.body) as List<dynamic>;
return data
.map(
(item) => Product.fromJson(
item as Map<String, dynamic>,
),
)
.toList();
}
Đây là ví dụ minh họa, trong đó endpoint được thay bằng địa chỉ giả example.com. Trong dự án thật, cần sử dụng địa chỉ API và cấu trúc response mà Backend cung cấp.
Đoạn code trên thể hiện sự phối hợp giữa HTTP và JSON Serialization / Deserialization. HTTP đảm nhiệm việc gửi yêu cầu và nhận phản hồi; JSON là định dạng dữ liệu được trao đổi; còn jsonDecode() và Product.fromJson() giúp chuyển dữ liệu nhận được thành dạng mà chương trình Dart sử dụng thuận tiện.
Ngoài ra, trong ứng dụng thực tế, cần xử lý thêm các trường hợp như timeout, lỗi kết nối, phản hồi không phải JSON, cấu trúc dữ liệu không đúng như mong đợi và lỗi chuyển đổi kiểu dữ liệu. Không nên chỉ kiểm tra mã trạng thái rồi mặc định phần body luôn hợp lệ.
10. Xử lý dữ liệu thiếu, null và sai kiểu dữ liệu
Một trong những vấn đề thường gặp khi làm việc với API là dữ liệu thực tế không luôn giống hoàn toàn với dữ liệu mà lập trình viên mong đợi.
Ví dụ, lớp Dart quy định name là String bắt buộc, nhưng Backend có thể trả về:
{
"id": 1,
"name": null,
"price": 250000
}
Nếu chương trình cố chuyển null thành String không nullable, quá trình Deserialization có thể thất bại.
Tương tự, nếu lớp Dart yêu cầu id là số nguyên nhưng API trả về "id": "1" dưới dạng chuỗi, phép ép kiểu trực tiếp sang int sẽ không hoạt động như mong đợi.
Để xử lý, trước tiên cần xác định trường nào bắt buộc, trường nào có thể thiếu hoặc nhận null, và kiểu dữ liệu mà Backend thực sự cam kết trả về.
Ví dụ, nếu mô tả sản phẩm không bắt buộc phải có, lớp Dart có thể khai báo:
class Product {
final int id;
final String name;
final String? description;
Product({
required this.id,
required this.name,
this.description,
});
}
Dấu ? trong String? cho biết thuộc tính description có thể nhận một chuỗi hoặc null.
Phương thức fromJson() có thể xử lý trường này như sau:
factory Product.fromJson(
Map<String, dynamic> json,
) {
return Product(
id: json['id'] as int,
name: json['name'] as String,
description: json['description'] as String?,
);
}
Cách này phù hợp khi description có thể là null hoặc không có trong dữ liệu. Tuy nhiên, nếu id hoặc name là trường bắt buộc, không nên tùy tiện gán giá trị mặc định chỉ để tránh lỗi. Làm như vậy có thể khiến ứng dụng hiển thị dữ liệu sai mà không phát hiện được vấn đề ở Backend.
Nếu API thực sự có thể trả số dưới dạng chuỗi, ứng dụng cần quy định rõ cách chuyển đổi và kiểm tra dữ liệu thay vì ép kiểu tùy tiện. Ví dụ, có thể dùng int.tryParse() khi đầu vào là chuỗi số, đồng thời báo lỗi nếu giá trị không hợp lệ.
Một nguyên tắc quan trọng là: Serialization / Deserialization không thay thế việc kiểm tra tính hợp lệ của dữ liệu. Việc chuyển đổi thành công không có nghĩa dữ liệu đã đúng về mặt nghiệp vụ. Chẳng hạn, price có thể được chuyển thành double thành công nhưng vẫn là một số âm không hợp lệ đối với giá bán sản phẩm.
11. Những lỗi thường gặp khi xử lý JSON
11.1. Gọi sai kiểu dữ liệu
Lập trình viên có thể nghĩ rằng API trả về một object nhưng thực tế API trả về một mảng. Khi đó, việc ép kiểu sang Map<String, dynamic> sẽ gây lỗi.
Cách khắc phục là kiểm tra cấu trúc response thực tế, chẳng hạn xem dữ liệu cấp cao nhất là object hay array, rồi mới lựa chọn cách chuyển đổi phù hợp.
11.2. Thiếu khóa hoặc tên khóa không khớp
Ví dụ, Backend trả về product_name, nhưng Flutter lại đọc name. Khi đó, json['name'] có thể trả về null dù dữ liệu sản phẩm thực sự có tên.
Để tránh vấn đề này, Frontend và Backend cần thống nhất cấu trúc JSON, bao gồm tên khóa, kiểu dữ liệu và quy tắc xử lý trường tùy chọn.
11.3. Nhầm giữa int và double
Dart phân biệt các kiểu số nguyên và số thực. Nếu một trường được khai báo là double, nhưng dữ liệu được ép trực tiếp từ int, chương trình có thể gặp lỗi kiểu dữ liệu.
Đối với trường dữ liệu có thể nhận cả số nguyên và số thực, có thể xử lý thông qua num rồi chuyển sang kiểu mong muốn, ví dụ (json['price'] as num).toDouble(). Tuy nhiên, vẫn cần xác nhận rằng giá trị thực sự là số.
11.4. JSON không hợp lệ
Nếu chuỗi đầu vào bị thiếu dấu ngoặc, sai dấu nháy hoặc không đúng cú pháp JSON, jsonDecode() sẽ báo lỗi.
Nguyên nhân có thể xuất phát từ dữ liệu bị lỗi, Backend trả về nội dung khác với dự kiến hoặc ứng dụng cố phân tích một trang HTML thông báo lỗi như thể đó là JSON.
Cách xử lý là kiểm tra phản hồi HTTP, xác nhận định dạng dữ liệu và bắt các ngoại lệ phù hợp khi cần. Không nên cho rằng mọi response đều là JSON hợp lệ.
11.5. Dữ liệu lồng nhau chưa được chuyển đổi
Nếu JSON chứa một danh sách sản phẩm, mỗi sản phẩm lại có thông tin thương hiệu, việc chuyển đổi lớp sản phẩm không tự động biến trường thương hiệu thành đối tượng Brand.
Lập trình viên cần xây dựng lớp phù hợp và gọi Brand.fromJson() cho phần dữ liệu tương ứng, hoặc sử dụng công cụ sinh code hỗ trợ các lớp lồng nhau.
11.6. Thay đổi cấu trúc API nhưng không cập nhật model
Nếu Backend đổi tên trường, thay đổi kiểu dữ liệu hoặc chuyển response sang một cấu trúc khác, các phương thức chuyển đổi cũ có thể không còn hoạt động.
Vì vậy, model phía Flutter cần được cập nhật đồng bộ với hợp đồng API. Với dự án lớn, có thể dùng tài liệu OpenAPI hoặc các quy ước API rõ ràng để giảm nguy cơ sai khác giữa Frontend và Backend.
12. Viết fromJson() và toJson() thủ công hay sử dụng công cụ sinh code?
Trong các ví dụ cơ bản, lập trình viên thường tự viết fromJson() và toJson(). Cách này phù hợp với những lớp ít thuộc tính, dễ hiểu và thuận tiện khi mới học Dart.
Tuy nhiên, trong ứng dụng thực tế, một model có thể chứa hàng chục trường dữ liệu, nhiều đối tượng lồng nhau và những quy tắc chuyển đổi phức tạp. Nếu viết thủ công quá nhiều lớp, công việc sẽ tốn thời gian và dễ mắc lỗi như bỏ sót trường, gõ sai tên khóa hoặc xử lý kiểu dữ liệu không nhất quán.
Vì vậy, Dart và Flutter có thể sử dụng các thư viện hỗ trợ sinh code để tự động tạo phần lớn mã chuyển đổi JSON. Một lựa chọn phổ biến là json_serializable, thường được sử dụng cùng json_annotation và công cụ sinh code build_runner.
Về cơ bản, lập trình viên khai báo model và các chú thích cần thiết; sau đó công cụ tạo ra phần chuyển đổi tương ứng. Khi cấu trúc model thay đổi, lập trình viên cần chạy lại quá trình sinh code theo cách thiết lập của dự án.
Ví dụ minh họa cách khai báo model với json_serializable:
import 'package:json_annotation/json_annotation.dart';
part 'product.g.dart';
@JsonSerializable()
class Product {
final int id;
final String name;
final double price;
Product({
required this.id,
required this.name,
required this.price,
});
factory Product.fromJson(
Map<String, dynamic> json,
) => _$ProductFromJson(json);
Map<String, dynamic> toJson() =>
_$ProductToJson(this);
}
Trong ví dụ này, product.g.dart là tệp code được sinh tự động. Các hàm _$ProductFromJson và _$ProductToJson được công cụ tạo ra; lập trình viên không tự viết nội dung của chúng trong lớp trên.
Để sử dụng được ví dụ, dự án phải thêm các dependency cần thiết và chạy code generation theo hướng dẫn của thư viện. Chỉ sao chép đoạn code mà chưa cài đặt và sinh tệp tương ứng sẽ không đủ để chương trình biên dịch thành công.
Ưu điểm của code generation là giảm lượng mã viết tay, hỗ trợ những model phức tạp và hạn chế một số lỗi do thao tác thủ công. Tuy nhiên, nó không tự giải quyết mọi vấn đề về dữ liệu. Lập trình viên vẫn cần hiểu cấu trúc JSON, quy tắc ánh xạ trường, kiểu dữ liệu nullable và các trường hợp API trả dữ liệu không hợp lệ.
Đối với người mới học Flutter, nên hiểu cách tự viết fromJson() và toJson() trước. Sau khi hiểu bản chất, việc học các công cụ sinh code sẽ dễ dàng hơn vì có thể nhận ra công cụ đang tự động hóa công việc gì.
13. Phân biệt JSON Serialization với JSON Deserialization và HTTP
Ba khái niệm này thường được sử dụng cùng nhau nhưng không có cùng nhiệm vụ.
HTTP là giao thức quy định cách máy khách và máy chủ trao đổi yêu cầu, phản hồi và dữ liệu qua mạng. HTTP không bắt buộc nội dung body phải là JSON; body có thể chứa nhiều loại dữ liệu khác nhau.
JSON là định dạng biểu diễn dữ liệu. Nó quy định cách thể hiện object, array, chuỗi, số, Boolean và null dưới dạng văn bản có cấu trúc.
Serialization / Deserialization là các quá trình chuyển đổi dữ liệu giữa những cấu trúc trong chương trình và một định dạng biểu diễn hoặc truyền tải.
Ví dụ, khi Flutter gửi thông tin đơn hàng lên Laravel, ứng dụng có thể dùng toJson() để chuyển đối tượng đơn hàng thành Map, jsonEncode() để tạo chuỗi JSON rồi đưa chuỗi đó vào body của HTTP Request. Laravel nhận request và xử lý dữ liệu theo cấu trúc mà API quy định.
Ở chiều ngược lại, Laravel trả về HTTP Response có body chứa JSON. Flutter đọc body, gọi jsonDecode() và dùng Order.fromJson() để tạo đối tượng đơn hàng.
Vì vậy, có thể hiểu mối quan hệ giữa chúng như sau: HTTP đảm nhiệm việc trao đổi thông tin, JSON cung cấp định dạng dữ liệu thường dùng, còn Serialization / Deserialization giúp chuyển đổi dữ liệu giữa cách chương trình tổ chức nội bộ và định dạng trao đổi đó.
14. Một số lưu ý quan trọng khi làm việc với JSON trong dự án thật
Thứ nhất, phải thống nhất cấu trúc dữ liệu giữa Frontend và Backend. Nếu Backend trả về trường total_price, Flutter không nên mặc định rằng trường đó có tên totalPrice, trừ khi đã khai báo quy tắc ánh xạ tương ứng.
Thứ hai, cần xác định rõ trường bắt buộc và trường tùy chọn. Không phải thuộc tính nào cũng có thể nhận null, và cũng không phải mọi trường bị thiếu đều nên được thay thế bằng một giá trị mặc định.
Thứ ba, không nên xem JSON là dữ liệu đáng tin cậy tuyệt đối. Dữ liệu từ API có thể bị sai định dạng, thiếu trường hoặc không thỏa mãn quy tắc nghiệp vụ. Backend cũng cần xác thực dữ liệu đầu vào trước khi lưu vào cơ sở dữ liệu.
Thứ tư, không đưa thông tin nhạy cảm vào JSON một cách tùy tiện. Serialization không phải mã hóa bảo mật. Những dữ liệu như mật khẩu, token xác thực hoặc thông tin cá nhân cần được xử lý theo các nguyên tắc bảo mật phù hợp.
Thứ năm, khi dữ liệu lớn hoặc cấu trúc phức tạp, nên cân nhắc hiệu năng và cách tổ chức model. Việc chuyển đổi nhiều dữ liệu có thể tốn thời gian và bộ nhớ, vì vậy cần tránh thực hiện những thao tác không cần thiết, đặc biệt trên luồng giao diện.
Thứ sáu, nên kiểm thử các phương thức fromJson() và toJson(). Các trường hợp cần kiểm tra có thể bao gồm dữ liệu hợp lệ, trường tùy chọn bị thiếu, trường nhận null, dữ liệu lồng nhau, sai kiểu dữ liệu và cấu trúc JSON không đúng như dự kiến.
Cuối cùng, không nên chỉ học thuộc tên các hàm. Điều quan trọng là hiểu dữ liệu đang ở dạng nào trước khi chuyển đổi và sẽ trở thành dạng nào sau khi chuyển đổi. Khi nắm được điều này, việc đọc code Flutter có sử dụng API sẽ dễ hiểu hơn rất nhiều.
15. Kết luận
JSON Serialization / Deserialization là một phần quan trọng trong quá trình xây dựng ứng dụng Flutter có trao đổi dữ liệu với Backend hoặc lưu trữ dữ liệu dưới dạng JSON.
Serialization chuyển đối tượng hoặc dữ liệu trong chương trình thành một định dạng có thể lưu trữ hoặc truyền đi. Trong Flutter, cách làm phổ biến là sử dụng toJson() để chuyển đối tượng thành Map, sau đó dùng jsonEncode() để tạo chuỗi JSON.
Deserialization thực hiện chiều ngược lại. Ứng dụng sử dụng jsonDecode() để phân tích chuỗi JSON thành các cấu trúc dữ liệu Dart, rồi dùng fromJson() để tạo đối tượng phù hợp với model đã khai báo.
Khi làm việc với REST API, hai quá trình này giúp Flutter trao đổi dữ liệu với Backend một cách có tổ chức. Chúng được sử dụng khi lấy danh sách sản phẩm, đăng nhập, gửi đơn hàng, cập nhật hồ sơ hoặc xử lý các dữ liệu nghiệp vụ khác.
Để sử dụng hiệu quả, lập trình viên cần hiểu cấu trúc JSON, kiểu dữ liệu của Dart, cách xử lý null, dữ liệu lồng nhau và những lỗi có thể xảy ra khi dữ liệu thực tế khác với mong đợi. Với model đơn giản, có thể viết phương thức chuyển đổi thủ công; với dự án lớn, có thể sử dụng công cụ sinh code để giảm mã lặp và hỗ trợ bảo trì.
Tóm lại, JSON Serialization / Deserialization không chỉ là việc gọi hai hàm jsonEncode() và jsonDecode(). Bản chất của nó là chuyển đổi dữ liệu giữa những cách biểu diễn khác nhau để các thành phần trong ứng dụng có thể trao đổi và sử dụng dữ liệu đúng cách.
16. Tài liệu tham khảo
- Dart – Using JSON: https://dart.dev/libraries/serialization/json
- Dart API –
dart:convert: https://api.dart.dev/dart-convert/dart-convert-library.html - Flutter – Networking: https://docs.flutter.dev/cookbook/networking
json_serializabletrên pub.dev: https://pub.dev/packages/json_serializablejson_annotationtrên pub.dev: https://pub.dev/packages/json_annotation








Khoá học lập trình game con rắn cho trẻ em




