Swift — Codable (1)

Hoàng Minh Nhật
Living is Sharing
Published in
5 min readJul 9, 2019

--

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 instace 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 các JSON có dạng đơn giản thế này, công việc của chúng ta chỉ là define Swift struct conform 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 nhất 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 sau 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.

Part 2

Sample playground: Swift_Codable.playground

--

--