Skip to main content

Swift - Codable (P1)

Codable được giới thiệu cùng với phiên bản 4.0 của Swift, đem lại sự thuận tiện cho người dùng mỗi khi cần encode/ decode giữa JSON và Swift object.

Codable là alias của 2 protocols: Decodable & Encodable
. Decodable: Chuyển data dạng string, bytes... sang instance (decoding/ deserialization)
. Encodable: Chuyển instance sang string, bytes... (encoding/ serialization)

Table of contents
. Swift Codable basic
. Swift Codable manual encode decode
. Swift Codable coding key
. Swift Codable key decoding strategy
. Swift Codable date decoding strategy
. Swift Codable nested unkeyed container

Swift Codable basic
Chúng ta sẽ đi vào ví dụ đầu tiên của Swift Codable, mục tiêu sẽ là convert đoạn JSON sau sang Swift object (struct)

{
    "name": "NhatHM",
    "age": 29,
    "page": "https://magz.techover.io/"
}

Cách làm:
Đối với JSON có dạng đơn giản thế này, công việc của chúng ta chỉ là define Swift struct to Codable protocol cho chính xác, sau đó dùng JSONDecoder() để decode về instance là được. Note: Nếu không cần phải chuyển ngược lại thành string/bytes (không cần encode) thì chỉ cần conform protocol Decodable là đủ.

Implementation: 

Define Swift struct:

struct Person: Codable {
    let name: String
    let age: Int
    let page: URL
    let bio: String?
}

Convert string to instance

// Convert json string to data
var data = Data(json.utf8)
let decoder = JSONDecoder()
// Decode json with dictionary
let personEntity = try? decoder.decode(Person.self, from: data)
if let personEntity = personEntity {
    print(personEntity)
}

Chú ý: Đối với dạng json trả về là array như dưới:

[{
    "name": "NhatHM",
    "age": 29,
    "page": "https://magz.techover.io/"
},
{
    "name": "RioV",
    "age": 19,
    "page": "https://nhathm.com/"
}]

thì chỉ cần define loại data sẽ decode cho chính xác là được:

let personEntity = try? decoder.decode([Person].self, from: data)

Ở đây ta đã định nghĩa được data decode ra sẽ là array của struct Person.

Swift Codable manual encode decode

Trong một vài trường hợp , data trả về mà chúng ta cần có thể nằm trong một key khác như dưới:

{
    "person": {
        "name": "NhatHM",
        "age": 29,
        "page": "https://magz.techover.io/"
    }
}

Trong trường hợp này, nếu define Swift struct đơn giản như phần 1 chắc chắn sẽ không thể decode được. Do đó cách làm sẽ là define struct sao cho nó tương đồng có thể với format của JSON. Ví dụ như đối với JSON ở trên, chúng ta có thể define struct như dưới:

Implementation

struct PersonData: Codable {
    struct Person: Codable {
        let name: String
        let age: Int
        let page: URL
        let bio: String? }
 
    let person: Person
}

Đối với trường hợp này, chúng ta vẫn sử dụng JSONDecoder() để decode string về instance như thường, tuy nhiên lúc sử dụng value của struct thì sẽ hơi bất tiện :

let data = Data(json.utf8)
let decoder = JSONDecoder()
let personEntity = try? decoder.decode(PersonData.self, from: data)
if let personEntity = personEntity {
    print(personEntity)
    print(personEntity.person.name)
}

Manual encode decode

Đối với dạng JSON data như này, chúng ta còn có một cách khác để xử lý data cho phù hợp, dễ dàng hơn như dưới:

Define struct (chú ý, lúc này không thể hiện struct conform to Codable nữa, mà sẽ conform to Encodable và Decodable một cách riêng biệt) :

struct Person {
    var name: String
    var age: Int
    var page: URL
    var bio: String? 
 
    enum PersonKeys: String, CodingKey {
        case person
    } 
 
    enum PersonDetailKeys: String, CodingKey {
        case name
        case age
        case page
        case bio
    }
}

Ở đây có một khái niệm mới là CodingKey. Về cơ bản, CodingKey chính là enum define các "key" mà chúng ta muốn Swift sử dụng để decode các value tương ứng. Ở đây key PersonKeys.person sẽ tương ứng với key "person" trong JSON string, các enum khác cũng tương tự (đọc thêm về CodingKey ở phần sau)

Với trường hợp này, ta sử dụng nestedContainer để đọc các value ở phía sâu của JSON, sau đó gán giá trị tương ứng cho properties của Struct.

Implementation

extension Person: Decodable {
    init(from decoder: Decoder) throws {
        let personContainer = try decoder.container(keyedBy: PersonKeys.self) 
        let personDetailContainer = try personContainer.nestedContainer(keyedBy: PersonDetailKeys.self, forKey: .person)
        name = try personDetailContainer.decode(String.self, forKey: .name)
        age = try personDetailContainer.decode(Int.self, forKey: .age)
        page = try personDetailContainer.decode(URL.self, forKey: .page)
        bio = try personDetailContainer.decodeIfPresent(String.self, forKey: .bio)
    }
}

Đây chính là phần implement để đọc ra các value ở tầng sâu của JSON, sau đó gán lại vào các properties tương ứng của struct. Các đoạn code trên có ý nghĩa như sau:

. personContainer là container tương ứng với toàn bộ JSON string
. personDetailContainer là container tương ứng với value của key person
. Nếu có các level sâu hơn thì ta lại tiếp tục sử dụng nestedContainer để đọc sâu vào trong
. Nếu một property nào đó (key value nào đó của json) mà có thể không trả về, thì sử dụng decodeIfPresent để decode (nếu không có value thì gán bằng nil)
Note: Đối với việc Encode thì cũng làm tương tự, tham khảo source code đi kèm (link cuối bài)
Với cách làm này, thì khi gọi đến properties của struct, đơn giản ta chỉ cần personEntity.name là đủ.

Swift Codable coding key
Trong đa số các trường hợp thì client sẽ sử dụng json format mà server đã định sẵn, do đó có thể gặp các kiểu json có format như sau:

{
    "person_detail": {
        "first_name": "Nhat",
        "last_name": "Hoang",
        "age": 29,
        "page": "https://magz.techover.io/"
    }
}

Đối với kiểu json như này, để struct có thể codable được thì cần phải define properties dạng person_detail, first_name. Điều này vi phạm vào coding convention của Swift. Trong trường hợp này chúng ta sử dụng Coding key để mapping giữa properties của Struct và key của JSON.

Implementation

struct Person {
    var firstName: String
    var lastName: String
    var age: Int
    var page: URL
    var bio: String? 
 
    enum PersonKeys: String, CodingKey {
        case person = "person_detail"
    } 
 
    enum PersonDetailKeys: String, CodingKey {
        case firstName = "first_name"
        case lastName = "last_name"
        case age
        case page
        case bio
    }
}

Với trường hợp này, khi sử dụng đoạn code decode như

var personDetailContainer = personContainer.nestedContainer(keyedBy: PersonDetailKeys.self, forKey: .person)

hay:

try personDetailContainer.encode(firstName, forKey: .firstName)

thì khi đó, Swift sẽ sử dụng các key json tương ứng là person_detail hoặc first_name.




Comments

Popular posts from this blog

MVVM và VIPER: Con đường trở thành Senior

Trong bài viết trước chúng ta đã tìm hiểu về MVC và MVP để ứng dụng cho một iOS App đơn giản. Bài này chúng ta sẽ tiếp tục ứng dụng 2 mô hình MVVM và VIPER . Nhắc lại là ứng dụng của chúng ta cụ thể khi chạy sẽ như sau: Source code đầy đủ cho tất cả mô hình MVC, MVP, MVVM và VIPER các bạn có thể download tại đây . MVVM MVVM có thể nói là mô hình kiến trúc được rất nhiều các cư dân trong cộng đồng ưa chuộng. Điểm tinh hoa của kiến trúc này là ở ViewModel , mặc dù rất giống với Presenter trong MVP tuy nhiên có 2 điều làm nên tên tuổi của kiến trúc này đó là: ViewModel không hề biết gì về View , một ViewModel có thể được sử dụng cho nhiều View (one-to-many). ViewModel sử dụng Observer design pattern để liên lạc với View (thường được gọi là binding data , có thể là 1 chiều hoặc 2 chiều tùy nhu cầu ứng dụng). Chính đặc điểm này MVVM thường được phối hợp với các thư viện hỗ trợ Reactive Programming hay Event/Data Stream , đây là triết lý lập trình hiện đại và hiệu

Alamofire vs URLSession

Alamofire vs URLSession: a comparison for networking in Swift Alamofire and URLSession both help you to make network requests in Swift. The URLSession API is part of the foundation framework, whereas Alamofire needs to be added as an external dependency. Many  developers  doubt  whether it’s needed to include an extra dependency on something basic like networking in Swift. In the end, it’s perfectly doable to implement a networking layer with the great URLSession API’s which are available nowadays. This blog post is here to compare both frameworks and to find out when to add Alamofire as an external dependency. Build better iOS apps faster Looking for a great mobile CI/CD solution that has tons of iOS-specific tools, smooth code signing, and even real device testing? Learn more about Bitrise’s iOS specific solutions! This shows the real power of Alamofire as the framework makes a lot of things easier. What is Alamofire? Where URLSession can be found within the s

Fileprivate vs private: Giải thích sự khác biệt

Fileprivate vs private in Swift: The differences explained Fileprivate and private are part of the access control modifiers in Swift. These keywords, together with internal, public, and open, make it possible to restrict access to parts of your code from code in other source files and modules. The private access level is the lowest and most restrictive level whereas open access is the highest and least restrictive. The documentation of Swift will explain all access levels in detail to you, but in this blog post, I’m going to explain the differences between two close friends: fileprivate and private. Build better iOS apps faster Looking for a great mobile CI/CD solution that has tons of iOS-specific tools, smooth code signing, and even real device testing? Learn more about Bitrise’s iOS specific solutions! Open access is the highest (least restrictive) access level and private access is the lowest (most restrictive) access level. This will improve readability and mak