Cách xử lí JSON trong Golang

I. Lời nói đầu

Việc parsing JSON trong ngôn ngữ static như Golang sẽ gặp phải chút vấn đề. Giả sử ta có dữ liệu JSON, trong JSON đó có 1 key và 1 value, nhưng value đó có thể lồng 1 cặp key và 1 value khác và cứ như vậy. Vậy thì compiler của 1 ngôn ngữ static như Golang làm thế nào có thể làm việc được với JSON đó?

II. Unmarshaling và Marshaling

Nếu trường hợp ta biết trước được cấu trúc của JSON đó như thế nào thì giải pháp chính là ta sẽ tạo 1  struct có cấu trúc tương ứng với cấu trúc của JSON đó, nếu JSON đó có thêm key nào khác thì chỉ việc bỏ qua key đó.

1. Parsing từ JSON sang struct (Unmarshaling)

type App struct {
    Id string `json:"id"`
    Title string `json:"title"`
}

data := []byte(`
    {
        "id": "k34rAT4",
        "title": "My Awesome App"
    }
`)

var app App
err := json.Unmarshal(data, &app)

2. Parsing từ struct sang JSON (Marshal)

data, err := json.Marshal(app)

III. Struct Tags

Có thể bạn sẽ nhận ra rằng ở ví dụ trên struct của chúng ta có thêm các tags json ở bên phải, đó chính là các manh mối để giúp có việc thực hiện parsing diễn ra theo đúng như mong đợi.

1. Field name:

Có thể bạn đã biết rằng việc viết hoa chữ cái đầu tiên của 1 field name của 1 struct mang ý nghĩa field đó là public, tương tự viết thường là private. Nhưng dữ liệu từ JSON thì rất hiếm khi các key được viết hoa. Do do có sự khác việt đó nên ta cần cho thêm các manh mối cho việc parsing và giải pháp chính là các tags.

Dưới đây là ví dụ đơn giản về việc viết thêm tag cho struct để phục vụ cho việc parsing JSON

type MyStruct struct {
    SomeField string `json:"some_field"`
}

2. Parsing từ struct sang JSON, làm gì khi 1 field có giá trị empty (zero value)?

Zero value cho kiểu dữ liệu string sẽ là empty string, cho số là 0, cho map, slice, pointernil.

type MyStruct struct {
    SomeField string `json:"some_field"`
}

Nếu some_field == "" thì với tag như bên trên JSON kết quả sẽ là {"some_field": ""}

Tuy nhiên đôi khi đó không thực là những gì ta mong muốn, nếu ta muốn bỏ qua field bị empty để nó không xuất hiện trong JSON thì ta sẽ dùng đến omitempty

type MyStruct struct {
    SomeField string `json:"some_field,omitempty"`
}
  1. Skipping fields
type App struct {
    Id string `json:"id"`
    Password string `json:"-"`
}

Để parser/writer bỏ qua 1 field thì chỉ cần dùng - trong tag

4. Nested Fields

Trong Golang cho phép 1 struct lồng 1 struct khác. Với struct lồng như vậy thì vẫn có thể thêm các tag vào 1 cách bình thường để xử lí JSON

type App struct {
    Id string `json:"id"`
}

type Org struct {
    Name string `json:"name"`
}

type AppWithOrg struct {
    App
    Org
}

5. Pointers

Con trỏ được dereference trước khi JSON bị encode. Hay nói cách khác, cái ta có được là value chứ không phải pointer.
6. Xử lí lỗi
MarshalUnmarshal đều trả về err. Ta có thể dùng nó để xử lí khi có lỗi xảy ra

func MustMarshal(data interface{}) []byte {
    out, err := json.Marshal(data)
    if err != nil {
        panic(err)
    }

    return out
}

IV. Parsing JSON mà không biết trước cấu trúc

Vậy trường hợp ta hoàn toàn không biết trước cấu trúc cụ thể của JSON thì ta phải xử lí như thế nào?  Việc định nghĩa struct là bất khả thì. Do đó ta sẽ thử dùng interface thay vì struct vì trong Golang 1 empty interface có thể chứa tất cả mọi thứ. Đợi đến thời điểm runtime thì compiler sẽ cung cấp memory phù hợp có những thứ đó. Khi đó code của chúng ta sẽ trông như thế này:

var parsed interface{}
err := json.Unmarshal(data, &parsed)

Tuy nhiên khi đã có được biến parsed, việc dùng biến này cần 1 chút xử lí để biết được kiểu dữ liệu cụ thể cụ nó là gì và việc này hơi tốn công. Ta cần thêm code check type như sau:

switch parsed.(type) {
    case int:
        someGreatIntFunction(parsed.(int))
    case map:
        someMapThing(parsed.(map))
    default:
        panic("JSON type not understood")
}

hoặc là

intVal, ok := parsed.(int)
if !ok {
    panic("JSON value must be an int")
}

Hiếm khi ta hoàn toàn không biết gì về JSON cần xử lí. Ví dụ như ta biết 1 thông tin nhỏ là kiểu dữ liệu của nó là object. Lúc này ta sẽ dùng map[string]interface{}, việc dùng map giúp ta có thêm khả năng refer đến value bằng key

var parsed map[string]interface{}

data := []byte(`
    {
        "id": "k34rAT4",
        "age": 24
    }
`)

err := json.Unmarshal(data, &parsed)

Và có thể lấy value từ key

parsed["id"]

Vì ta dùng map[string]interface{} nên khi lấy value từ map đó bằng 1 key thì kiểu dữ liệu của value đó vẫn là interface, điều này có nghĩa là ta vẫn sẽ cần check type cho interface đó.
Golang dùng 6 loại dữ liệu sau cho tất cả các values được parse thành interface

bool, cho JSON booleans
float64, cho JSON numbers
string, cho JSON strings
[]interface{}, cho JSON arrays
map[string]interface{}, cho JSON objects
nil cho JSON null

Điều này có nghĩa là nếu ở JSON, value là số, thì khi parse value đó luôn luôn có kiểu là float64, nếu ta muốn thành int thì phải xử lí thêm.

V. Tài liệu tham khảo

  1. https://golang.org/pkg/encoding/json/
  2. https://eager.io/blog/go-and-json/