Lỗi
Bạn đang decode JSON vào một struct Go và đột nhiên gặp phải lỗi như thế này:
json: cannot unmarshal string into Go value of type int
json: cannot unmarshal number into Go value of type string
json: cannot unmarshal object into Go value of type []string
Lỗi này xảy ra bên trong json.Unmarshal hoặc json.NewDecoder(...).Decode(...). Chỉ một field không khớp kiểu dữ liệu là đủ phá hỏng toàn bộ quá trình — không có gì được decode cả.
Nguyên nhân
Package encoding/json của Go không tự động chuyển đổi kiểu dữ liệu. Nếu struct của bạn khai báo kiểu int nhưng JSON trả về "42" — một chuỗi có dấu ngoặc kép — decoder sẽ từ chối chuyển đổi. Không có ngoại lệ. Điều ngược lại cũng vậy: một số JSON thuần sẽ không thể tự động vào được field kiểu string.
Ba tình huống thường gây ra lỗi này nhất:
- API bên thứ ba trả về số dưới dạng chuỗi (ví dụ:
"id": "123"thay vì"id": 123) — điều này cực kỳ phổ biến với các payment API và REST service cũ - Database hoặc hệ thống legacy serialize mọi cột thành chuỗi
- Dữ liệu form từ frontend khi JavaScript ép kiểu số thành chuỗi có ngoặc kép trước khi gửi đi
Cách Sửa Từng Bước
Bước 1 — Xác định field không khớp kiểu
Thông báo lỗi cho biết kiểu Go mà decoder mong đợi. So sánh với struct của bạn để tìm ra field đó. Ví dụ cụ thể:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
// JSON nhận từ API:
// {"id": "99", "name": "Alice", "age": 30}
// ^ "id" là chuỗi trong JSON nhưng int trong struct → gây ra lỗi
Bước 2 — Phương án A: Sửa nguồn JSON (khuyến nghị)
Bạn có quyền kiểm soát API hoặc nguồn dữ liệu? Hãy sửa ngay ở đó. Số trong JSON phải để không có dấu ngoặc kép:
// Sai
{"id": "99", "age": "30"}
// Đúng
{"id": 99, "age": 30}
Xử lý tận gốc. Không cần giải pháp vá víu nào cả.
Bước 3 — Phương án B: Dùng field kiểu string + tự chuyển đổi
Không thể sửa nguồn? Đổi field trong struct thành kiểu string, unmarshal sạch sẽ, rồi tự tay chuyển đổi:
import (
"encoding/json"
"fmt"
"strconv"
)
type User struct {
ID string `json:"id"` // nhận chuỗi từ JSON
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
data := []byte(`{"id": "99", "name": "Alice", "age": 30}`)
var u User
if err := json.Unmarshal(data, &u); err != nil {
panic(err)
}
// Chuyển đổi sau khi unmarshal
id, err := strconv.Atoi(u.ID)
if err != nil {
panic(fmt.Sprintf("invalid id: %v", err))
}
fmt.Println(id) // 99
}
Bước 4 — Phương án C: Dùng json.Number cho field số linh hoạt
json.Number thực chất là kiểu string có thể chứa bất kỳ số JSON nào. Bạn lấy giá trị thực ra một cách tường minh:
import (
"encoding/json"
"fmt"
)
type User struct {
ID json.Number `json:"id"`
Name string `json:"name"`
}
func main() {
data := []byte(`{"id": 99, "name": "Alice"}`)
var u User
json.Unmarshal(data, &u)
id, _ := u.ID.Int64()
fmt.Println(id) // 99
}
Bước 5 — Phương án D: Tự triển khai UnmarshalJSON
Một số API không nhất quán — "id" có lúc trả về 99, có lúc lại là "99". Một custom unmarshaler sẽ xử lý được cả hai trường hợp:
import (
"encoding/json"
"strconv"
)
type FlexInt int
func (f *FlexInt) UnmarshalJSON(b []byte) error {
// Thử parse như số trước
var n int
if err := json.Unmarshal(b, &n); err == nil {
*f = FlexInt(n)
return nil
}
// Fallback sang chuỗi
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
n, err := strconv.Atoi(s)
if err != nil {
return err
}
*f = FlexInt(n)
return nil
}
type User struct {
ID FlexInt `json:"id"`
Name string `json:"name"`
}
func main() {
// Hoạt động với cả {"id": 99} và {"id": "99"}
data := []byte(`{"id": "99", "name": "Alice"}`)
var u User
if err := json.Unmarshal(data, &u); err != nil {
panic(err)
}
// u.ID là 99
}
Kiểm Tra Kết Quả
Cách đơn giản nhất — kiểm tra err sau khi decode:
var u User
if err := json.Unmarshal(data, &u); err != nil {
fmt.Println("vẫn còn lỗi:", err)
} else {
fmt.Printf("decoded: %+v\n", u)
}
Tốt hơn nữa, hãy viết unit test. Nó tự động phát hiện regression sau này:
func TestDecodeUser(t *testing.T) {
data := []byte(`{"id": "99", "name": "Alice", "age": 30}`)
var u User
if err := json.Unmarshal(data, &u); err != nil {
t.Fatal(err)
}
if int(u.ID) != 99 {
t.Fatalf("expected 99, got %d", u.ID)
}
}
Mẹo Hay
Kiểm tra JSON payload trước khi định nghĩa struct
Response từ API thực tế thường lộn xộn. Trước khi khai báo bất kỳ struct Go nào, hãy format JSON và xem xét kỹ. Tôi dùng ToolCraft's JSON Formatter — paste response thô vào là thấy ngay số nào có dấu ngoặc kép, số nào không. Mọi thứ chạy trên trình duyệt; không có gì được upload lên server, điều quan trọng khi payload chứa dữ liệu người dùng.
Field lồng nhau có thể che giấu nguyên nhân thực sự
Sự không khớp kiểu không phải lúc nào cũng ở cấp độ ngoài cùng. Một field nằm sâu ba cấp vẫn có thể gây ra lỗi tương tự. In kiểu của lỗi ra để xem chi tiết hơn:
var u User
if err := json.Unmarshal(data, &u); err != nil {
fmt.Printf("type: %T, detail: %v\n", err, err)
}
Struct tag phân biệt chữ hoa chữ thường
Tag json:"fieldname" phải khớp chính xác với key trong JSON — bao gồm cả cách viết hoa/thường. Theo quy ước, field của Go struct được export (viết hoa), nhưng REST API hầu như luôn gửi key viết thường. Tag như json:"UserID" sẽ không khớp với "userid" trong payload.
Đừng dùng interface{} như một lối thoát
Decode vào map[string]interface{} sẽ tránh được lỗi này. Nhưng không phải miễn phí. Các giá trị số sẽ âm thầm trở thành float64, bạn mất hoàn toàn tính an toàn về kiểu dữ liệu, và mỗi lần truy cập field phía sau đều cần type assertion. Dùng struct có kiểu rõ ràng sẽ verbose hơn lúc đầu nhưng ít đau đầu hơn nhiều về sau.

