Dạo này mình hơi bận nên bỏ bê blog quá. Hôm nay quyết định chăm chút trở lại bằng một series về Codable, coi như làm nóng bản thân :D. Đã có một bài mình đề cập đến vấn đề này. Tuy nhiên bài viết đó chỉ ở mức giới thiệu, lần này mình sẽ tăng độ khó cho game thêm chút nữa.

Codable

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let json = """
{
    "userName": "crossover",
    "position": "SG",
    "id": 234
}
""".data(using: .utf8)!

struct Baller {
    let userName: String
    let position: String
    let id: Int
}

Giả sử ta có 1 json đơn giản và struct tương ứng như trên. Để deserialization json nói trên, trước đây, ta phải parse một cách thủ công. Đối với những dự án lớn, dữ liệu trả về phức tạp, thực hiện việc parse json bằng tay vừa tiêu tốn của ta kha khá thời gian, vừa khiến ta thấy nhàm chán. Tuy nhiên từ khi swift 4 xuất hiện, chúng ta đã có giải pháp mang tên Codable.

Codable là tính năng mới được đưa thêm vào trong Swift 4 nhằm hỗ trợ serializationdeserialization, giúp ta tự động hóa công việc nhàm chán này. Bản thân Codable là sự kết hợp của 2 protocol EncodeDecode mà mình sẽ trình bày ngay sau đây.

Decodable

Decodable là protocol được định nghĩa như sau

1
2
3
4
public protocol Decodable {
    // ...
    init(from decoder: Decoder) throws
}

Theo định nghĩa này ta sẽ implement hàm init trong protocol để thực hiện deserialization cho đối tượng.

Trở lại ví dụ ngay đầu bài viết, ta sẽ gán protocol Decodable cho Baller để tự động việc parse json sang model.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
extension Baller: Decodable {
    // 1
    enum CodingKeys: String, CodingKey {
        case userName
        case position
        case id
    }
    
    // 2
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        userName = try container.decode(String.self, forKey: .userName)
        position = try container.decode(String.self, forKey: .position)
        id = try container.decode(Int.self, forKey: .id)
    }
}

// 3
let decoder = JSONDecoder()
let instance = try decoder.decode(Baller.self, from: json)
instance.userName // "crossover"

Có 3 bước cơ bản để làm công việc trên:

  • B1: tạo enum kế thừa lại CodingKey. Để ý một chút, các case của enum CodingKeys mapping với key của json. Đây là điều kiện bắt buộc nếu ta muốn parse json thành công. Tên properties của struct Baller không nhất thiết phải trùng với tên key của json, nhưng để tránh nhầm lẫn và khó hiểu, ta đặt tên properties trùng với các key trong json. Chúng ta sẽ thống nhất cách thức này xuyên suốt bài viết.

  • B2: implement hàm init(from decoder: Decoder) throws. Ở bước này ta phải tạo container trước khi decodable. Container có thể chia làm 3 loại, tuy nhiên ở bước này mình sẽ không đi sâu chi tiết cả 3 loại mà chỉ đề cập đến trọng tâm trong ví dụ ở trên. Container đang dùng có kiểu KeyedDecodingContainer, áp dụng trong trường hợp properties trong đối tượng cần parse đều có các key tương ứng. Công thức tổng quát sẽ như sau:

1
2
let container = try decoder.container(keyedBy: CodingKeys.self)
// decode to initialize properties ...
  • B3: Bước này cũng là bước quan trọng nhất, cho ta kết quả của cả quá trình. Ở bước này ta sử dụng JSONDecoder như đoạn code ở trên.

Decodable ngắn gọn

Trên đây là các bước cần thiết khi ta sử dụng Decodable để parse dữ liệu. Tuy nhiên ta có thể để compiler trợ giúp ta một số công đoạn viết code.

1
2
3
4
extension Baller: Decodable {}
let decoder = JSONDecoder()
let instance = try decoder.decode(Baller.self, from: json)
instance.userName // "crossover"

Như đoạn code trên ta không phải tạo CodingKey, không phải implement hàm init(from decoder: Decoder) throws, chỉ phải làm mỗi bước thứ 3.

keyDecodingStrategy

Một số trường hợp đặc biệt, convention ở backend sử dụng snake_case (key “userName” ở trên thay đổi thành “user_name”)

1
2
3
// ...
"user_name": "crossover",
// ...

Trong khi đó mobile sử dụng camelCase, ta cũng có thể xử lý ngắn gọn dựa vào thuộc tính keyDecodingStrategy của JSONDecoder mà không phải đặt tên property theo convention của backend.

1
2
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

dateDecodingStrategy

Một tình huống cũng hay gặp phải là ta phải parse dữ liệu dạng Date mà json thì không có format date, time rồi. Chưa kể trường hợp phải convert từ string sang date nữa. JSONDecoder có thuộc tính hỗ trợ ta trong việc convert string sang dạng date là dateDecodingStrategy

Giả sử ta cần parse dữ liệu như sau:

1
2
3
4
{
  "checkin": "2019-04-26T07:51:30+0000",
  ...
}

Ta có thể dùng đoạn code sau để convert string về type Date

decoder.dateDecodingStrategy = .iso8601

Mình đang sử dụng chuẩn .iso8601, nếu bạn muốn tùy biến format thì có thể dùng type .formatted(<#DateFormatter#>)

Encodable

Giờ đến lúc ta bàn về chức năng serialization bằng cách sử dụng Encodable. Cũng như Decodable, Encodable là protocol được định nghĩa như sau:

1
2
3
public protocol Encodable {
    func encode(to encoder: Encoder) throws
}

Ta sẽ implement lại phương thức này đối với Baller

1
2
3
4
5
6
7
8
extension Baller: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(userName, forKey: .userName)
        try container.encode(position, forKey: .position)
        // ...
    }
}

Chúng ta cũng sẽ tạo container, tuy nhiên thay vì sử dụng let thì ở đây ta phải sử dụng var. Các bước encode thực hiện như ở trên.

Cuối cùng ta sử dụng JSONEncoder để đưa ra dữ liệu mong muốn

1
2
3
let encoder = JSONEncoder()
let data = try encoder.encode(instance)
print(String(data: data, encoding: .utf8)!)

Kết quả của đoạn code trên sẽ in ra dòng

{"position":"SG","userName":"crossover"}

Để kết quả trông đẹp hơn ta có thể sử dụng thuộc tính tronng JSONEncoder như sau:

1
encoder.outputFormatting = .prettyPrinted

Khi đó kết quả sẽ ra

{
    "position" : "SG",
    "userName" : "crossover"
}

Nested json

Decode

Mình đã hoàn thành trường hợp đơn giản nhất, giờ mình sẽ đi vào trường hợp phức tạp hơn. Như ta đã biết, trong các dự án thực tế, dữ liệu trả về không đơn giản như ở ví dụ đầu tiên, cấu trức của nó thường phức tạp hơn. Chúng ta sửa lại json đầu tiên một chút:

1
2
3
4
5
6
7
8
9
let json = """
{
    "payload": {
        "userName": "crossover",
        "position": "SG",
        "id": 234
    }
}
""".data(using: .utf8)!

Json ban đầu được lồng vào trong 1 json khác, có key là “payload”. Giờ không thể deserialization trực tiếp nữa. Ta phải sửa lại code như sau

1
2
// let instance = try decoder.decode(Baller.self, from: json)
let instance = try decoder.decode([String:Baller].self, from: json)

Đây là cách đơn giản nhất, tuy nhiên lúc này dữ liệu trả về không phải là chính đối tượng mà nó đã được lồng vào trong 1 dictionary. Để lấy dữ liệu ra ta phải làm như sau

1
2
instance["baller"] // Baller
instance["baller"]?.userName // "crossover"

Có một cách khác mà mình thích hơn, viết ra một cách tường minh hơn và dữ liệu được lấy ra trực tiếp chứ không bị lồng vào trong 1 đối tượng khác.

Ta phải làm thêm một bước nữa so với trường hợp cơ bản ban đầu. Ngoài việc định nghĩa các key mapping với properties của đối tượng, ta định nghĩa cả key mapping bao bên ngoài đối tượng. Có bao nhiêu tầng lớp bên ngoài thì có bấy nhiêu CodingKey được định nghĩa thêm.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
extension Baller: Decodable {
    enum OuterKeys: CodingKey {
        case payload
    }

    enum InnerKeys: CodingKey {
        case userName
        case position
        case id
    }

    //...
}

Trường hợp cụ thể trong bài viết, ta có duy nhất một key bao bên ngoài, nên mình định nghĩa OuterKeys cho key bao ngoài (payload), và InnerKeys cho đối tượng cần xử lý.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
init(from decoder: Decoder) throws {
    // 1.
    let outer = try decoder.container(keyedBy: OuterKeys.self)
    // 2.
    let inner = try outer.nestedContainer(keyedBy: InnerKeys.self, forKey: .payload)
    
    userName = try inner.decode(String.self, forKey: .userName)
    position = try inner.decode(String.self, forKey: .position)
    id = try inner.decode(Int.self, forKey: .id)
}

Ta chú ý 2 bước ban đầu:

let outer = try decoder.container(keyedBy: OuterKeys.self)

Bước đầu tiên này ta cũng lấy ra container nhưng cho OuterKeys. Lúc này ta lấy được nested json bên trong payload

1
2
3
{
    "payload": ...
}

Bước đầu này đưa ta trở về trường hợp cơ bản nêu đầu bài viết, lúc này ta cần phải lấy container cho đối tượng cần xử lý. Nó nằm trong outer nên mình sử dụng phương thức nestedContainer

let inner = try outer.nestedContainer(keyedBy: InnerKeys.self, forKey: .payload)

Encode

Encode chúng ta làm tương tự, cần tạo outer container và inner container như lúc decode.

1
2
3
4
5
6
7
8
9
extension Baller: Encodable {
    func encode(to encoder: Encoder) throws {
        var outter = encoder.container(keyedBy: OuterKeys.self)
        var inner = outter.nestedContainer(keyedBy: InnerKeys.self, forKey: .payload)
        try inner.encode(userName, forKey: .userName)
        try inner.encode(position, forKey: .position)
        // ...
    }
}

Chúng ta tạm kết thúc bài viết ở đây, khi nào có hứng mình lại viết tiếp :D.