Phân tích cú pháp JSON trong Swift là một việc phổ biến phải làm. Hầu hết mọi ứng dụng đều giải mã JSON để hiển thị dữ liệu theo cách trực quan. Phân tích cú pháp JSON chắc chắn là một trong những điều cơ bản bạn nên học với tư cách là một lập trình viên iOS.

Khóa học Lập trình di động iOS Swift tại Techmaster

Giải mã JSON trong Swift khá dễ dàng và không yêu cầu bất kỳ phụ thuộc bên ngoài nào. Các API cơ bản đi kèm với Swift sẽ đủ để thực hiện công việc, vì vậy chúng ta hãy cùng tìm hiểu nhé!

Khái niệm cơ bản về giải mã JSON (JSON decoding)

Thật tốt khi bắt đầu với những điều cơ bản để bạn hiểu cách phân tích cú pháp JSON trong Swift hoạt động như thế nào. Hãy lấy ví dụ sau về bài đăng trên blog SwiftLee:

{
	"title": "Optionals in Swift explained: 5 things you should know",
	"url": "https://www.avanderlee.com/swift/optionals-in-swift-explained-5-things-you-should-know/",
	"category": "Swift",
	"views": 47093
}

Chúng tôi có thể dễ dàng giải mã điều này bằng cách sử dụng giao thức Decodable:

struct BlogPost: Decodable {
    enum Category: String, Decodable {
        case swift, combine, debugging, xcode
    }

    let title: String
    let url: URL
    let category: Category
    let views: Int
}

Chúng tôi đã xác định một enum Category cũng tuân theo giao thức Decodable, và cả các thuộc tính khớp với tên từ khoá trong JSON đã xác định của chúng tôi. Mọi tuân thủ theo giao thức Decodable sẽ tự động chuyển đổi. Điều này có nghĩa là bạn cũng có thể sử dụng các loại Decodable được xác định tùy chỉnh của riêng mình như một thuộc tính.

Bằng cách sử dụng JSONDecoder, chúng ta có thể làm cho việc phân tích cú pháp JSON thực sự đơn giản:

let JSON = """
{
    "title": "Optionals in Swift explained: 5 things you should know",
    "url": "https://www.avanderlee.com/swift/optionals-in-swift-explained-5-things-you-should-know/",
    "category": "swift",
    "views": 47093
}
"""

let jsonData = JSON.data(using: .utf8)!
let blogPost: BlogPost = try! JSONDecoder().decode(BlogPost.self, from: jsonData)

print(blogPost.title) // Prints: "Optionals in Swift explained: 5 things you should know"

Mặc dù điều này có thể tạo ấn tượng rằng phân tích cú pháp JSON thực sự dễ dàng, nhưng tất cả đều thuộc về các trường hợp biên. May mắn thay, Swift cũng đủ khả năng để xử lý những điều đó.

Không bắt buộc phải xác định từng thuộc tính

Thật tốt khi biết rằng bạn không bắt buộc phải xác định từng thuộc tính đi kèm với JSON của mình. Điều này có nghĩa là cấu trúc sau cũng sẽ hoạt động:

struct BlogPost: Decodable {
    let title: String
}

Điều này thật tuyệt vì có thể bạn đang thêm các trường giá trị mới sau khi đã phát hành phiên bản ứng dụng của mình. Nếu nó không hoạt động như vậy, bạn có thể dễ dàng phá vỡ các phiên bản cũ.

Optional và JSON decoding

Có thể bạn không chắc liệu khóa JSON có được trả lại hay không hoặc liệu một giá trị có được đặt hay không, trong trường hợp này, bạn có thể xác định một thuộc tính Swift là Optional và JSONDecoder sẽ lo phần còn lại.

struct BlogPost: Decodable {
    let title: String
    /// Define a key as optional if it can be returned as `nil` or if it does not always exist in the JSON.
    let subtitle: String?
}

Giải mã mảng JSON trong Swift

Giải mã một mảng JSON (array JSON) trong Swift gần như dễ dàng như giải mã một đối tượng JSON. Lấy ví dụ JSON sau:

[{
    "title": "Optionals in Swift explained: 5 things you should know",
    "url": "https://www.avanderlee.com/swift/optionals-in-swift-explained-5-things-you-should-know/"
},
{
    "title": "EXC_BAD_ACCESS crash error: Understanding and solving it",
    "url": "https://www.avanderlee.com/swift/exc-bad-access-crash/"
},
{
    "title": "Thread Sanitizer explained: Data Races in Swift",
    "url": "https://www.avanderlee.com/swift/thread-sanitizer-data-races/"
}]

Chúng tôi có thể phân tích cú pháp danh sách các bài đăng trên blog này bằng cách xác định loại có thể giải mã là [BlogPost].self:

struct BlogPost: Decodable {
    let title: String
    let url: URL
}

let blogPosts: [BlogPost] = try! JSONDecoder().decode([BlogPost].self, from: jsonData)
print(blogPosts.count) // Prints: 3

Ánh xạ các khóa JSON với tên thuộc tính tùy chỉnh

Việc phân tích cú pháp JSON không phải lúc nào cũng dễ dàng như sao chép các khóa giống nhau vào một cấu trúc. Một điều khá phổ biến là bạn muốn xác định các tên thuộc tính khác nhau khi ánh xạ JSON.

Lấy ví dụ về JSON trước, có thể chúng tôi muốn đặt tên url là htmlLink trong mô hình JSON của chúng tôi. Chúng ta có thể tạo ánh xạ này bằng cách xác định một enum CodingKeys tùy chỉnh:

struct BlogPost: Decodable {
    enum Category: String, Decodable {
        case swift, combine, debugging, xcode
    }

    enum CodingKeys: String, CodingKey {
        case title, category, views
        // Map the JSON key "url" to the Swift property name "htmlLink"
        case htmlLink = "url"
    }

    let title: String
    let htmlLink: URL
    let category: Category
    let views: Int
}


let blogPost: BlogPost = try! JSONDecoder().decode(BlogPost.self, from: jsonData)
print(blogPost.htmlLink) // Prints: "https://www.avanderlee.com/swift/optionals-in-swift-explained-5-things-you-should-know/"

Như bạn có thể thấy, chúng tôi đã xác định một ánh xạ tùy chỉnh để chuyển đổi url khóa JSON thành htmlLink tên thuộc tính Swift.

Vì chúng tôi không thay đổi tên của title, category và views nên chúng tôi có thể giữ nguyên trường hợp này. Chúng tôi phải bao gồm các khóa đó vì JSONDecoder sẽ chuyển sang ánh xạ đã xác định của chúng tôi cho tất cả các thuộc tính đã xác định. Nếu chúng tôi không làm điều đó, chúng tôi sẽ gặp phải lỗi sau:

Type ‘BlogPost’ does not conform to protocol ‘Decodable’

Chuyển đổi giữa camel case và snack case

Một lý do phổ biến để xác định ánh xạ tùy chỉnh cho các khóa là phần phụ trợ bạn đang sử dụng sử dụng chữ hoa chữ thường để đặt tên cho các thuộc tính.

Trong Swift, chúng tôi chủ yếu sử dụng chữ hoa camel, nghĩa là chúng tôi bắt đầu bằng một chữ cái viết thường và sau đó viết hoa chữ cái đầu tiên của các từ tiếp theo: htmlLink hoặc numberOfBlogPosts. Các từ tương tự sử dụng snack case trông như sau: html_linknumber_of_blog_posts.

May mắn thay, chúng tôi không phải xác định ánh xạ tùy chỉnh cho từng khóa đã xác định. Lấy ví dụ JSON sau của blog:

{
    "title": "A weekly Swift Blog on Xcode and iOS Development - SwiftLee",
    "url": "https://www.avanderlee.com",
    "total_visitors": 378483,
    "number_of_posts": 47093
}

Chúng tôi có thể dễ dàng giải mã JSON đó bằng cách đặt keyEncodingStrategy của bộ giải mã của chúng tôi thành .convertFromSnakeCase:

struct Blog: Decodable {
    let title: String
    let url: URL
    let totalVisitors: Int
    let numberOfPosts: Int
}

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

let blog: Blog = try! decoder.decode(Blog.self, from: jsonData)
print(blog.numberOfPosts) // Prints: 47093

Điều đó thật dễ dàng! Điều này cũng hoạt động tốt với CodingKeys được xác định tùy chỉnh. Vì vậy, nếu bạn muốn ánh xạ url thành htmlLink giống như chúng tôi đã làm trước đây, bạn có thể dễ dàng thực hiện điều đó như sau:

struct Blog: Decodable {

    enum CodingKeys: String, CodingKey {
        case title, totalVisitors, numberOfPosts

        case htmlLink = "url"
    }

    let title: String
    let htmlLink: URL
    let totalVisitors: Int
    let numberOfPosts: Int
}

Giải mã ngày JSON với các định dạng tùy chỉnh

Ngày trong JSON được định nghĩa là Chuỗi hoặc khoảng thời gian và yêu cầu hàm chuyển đổi. Chúng tôi có thể thiết lập một hàm như vậy trên JSONDecoder của mình, giống như chúng tôi đã làm để chuyển đổi camel case thành snack case.

Lấy ví dụ JSON sau về một bài đăng trên blog:

{
    "title": "Optionals in Swift explained: 5 things you should know",
    "date": "2019-10-21T09:15:00Z"
}

Ngày trong ví dụ này được xác định với định dạng sau: yyyy-MM-dd'T'HH:mm:ss . Chúng tôi cần tạo DateFormatter tùy chỉnh với định dạng này và áp dụng điều này cho bộ giải mã của chúng tôi bằng cách đặt dateDecodingStrategy thành định dạng:

struct BlogPost: Decodable {
    let title: String
    let date: Date
}

let decoder = JSONDecoder()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
dateFormatter.locale = Locale(identifier: "en_US")
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
decoder.dateDecodingStrategy = .formatted(dateFormatter)

let blogPost: BlogPost = try! decoder.decode(BlogPost.self, from: jsonData)
print(blogPost.date) // Prints: 2019-10-21 09:15:00 +0000

Có một số hàm khác có sẵn để thiết lập:

  • deferredToDate: Sử dụng định dạng dữ liệu riêng của Apple để theo dõi số giây và mili giây kể từ ngày 1 tháng 1 năm 2001. Điều này chủ yếu hữu ích khi sử dụng trực tiếp với các nền tảng của Apple.
  • milisecondsSince1970: Định dạng này theo dõi số giây và mili giây kể từ ngày 1 tháng 1 năm 1970 và được sử dụng phổ biến hơn rất nhiều.
  • secondsSince1970: Theo dõi số giây kể từ ngày 1 tháng 1 năm 1970.
  • iso8601: Giải mã Ngày dưới dạng chuỗi có định dạng ISO-8601 (ở định dạng RFC 3339).

Tùy thuộc vào cách API bạn đang sử dụng trả về ngày mà bạn có thể chọn giữa các hàm đó.

Kết luận

Swift giúp giải mã JSON thực sự dễ dàng. Không cần sử dụng thư viện tùy chỉnh để phân tích cú pháp JSON vì API mặc định mang đến mọi thứ chúng ta cần, từ ánh xạ khóa tùy chỉnh đến ngày định dạng. 

Cảm ơn đã đón đọc!

Nguồn bài viết!

Techmaster mớ lớp, khai giảng khóa học lập trình di động iOS 3 tháng trong đầu tháng 5/2021

Chi tiết khóa học: https://ios.techmaster.vn/

Liên hệ: Ms Mẫn - 0963023185 (zalo)