Giới thiệu Property Wrapper trong Swift với Serializable ứng dụng trong parser dữ liệu từ JSON String

1. Parser dữ từ JSON String

Trong Swift chúng ta có thể sử dụng Codable để parser dữ liệu như sau:

Ví dụ có class Person:

class Person: Codable {
 var fullName: String?
 var district: String?
 
 required init() {}
 
 enum CodingKeys: String, CodingKey {
     case fullName = "full_name"
     case district
 }
}
person.swift

Khi sử dụng Codable của Swift trong trường hợp Field dữ liệu được định nghĩa khác với biến bạn khai báo thì cần có CodingKeys để Mapping dữ liệu, trong trường hợp trên ta có biến fullName và Field là full_name được định nghĩa ở trong json.

Cách sử dụng:

let json = """
            {
               "full_name": "Nguyen Van A",
               "district": "Vinh Phuc"
            }
         """
do {
   guard let data = json.data(using: .utf8) else {
      return
   }
   let jsonDecoder = JSONDecoder()
   let value = try jsonDecoder.decode(Person.self, from: data)
   print("My name is \(value.fullName ?? "")")
} catch {
   print("error \(error)")
}
ViewController.swift

Kêt quả trả về sẽ là:

Kết quả trả về
Kết quả trả về

Đây là cách tiếp cận của mọi người khi parser dữ liệu từ JSON string, tuy nhiên với Swift hiện tại chúng ta có một cách tiếp cận khác với vấn đề này một cách khác khá là cool.

Trong Swift 5.1, Apple giới thiệu Property Wrapper – một tính năng mạnh mẽ giúp bọc các thuộc tính trong một lớp hoặc cấu trúc trung gian để tự động hóa hoặc thêm logic tùy chỉnh cho việc get/set giá trị. Bài viết này sẽ hướng dẫn bạn cách sử dụng Property Wrapper để xây dựng Serializable, giúp ánh xạ dữ liệu từ JSON sang mô hình Swift một cách linh hoạt. Chúng ta cùng tìm hiểu xem Property Wrapper là gì nhé.

2. Property Wrapper là gì?

Property Wrapper trong Swift cho phép bạn thêm logic khi đọc/ghi dữ liệu của một thuộc tính mà không làm thay đổi cách sử dụng thuộc tính đó. Cú pháp chung của Property Wrapper như sau:

@propertyWrapper
class Wrapper<T> {
    private var value: T

    var wrappedValue: T {
        get { value }
        set { value = newValue }
    }

    init(wrappedValue: T) {
        self.value = wrappedValue
    }
}
wrapper.swift

Khi sử dụng Property Wrapper, bạn chỉ cần đánh dấu một thuộc tính bằng @Wrapper, và mọi thao tác get/set sẽ tuân theo logic được định nghĩa bên trong Wrapper.

3. Xây dựng Property Wrapper Serializable từng bước

Chúng ta sẽ từng bước xây dựng Serializable, giúp ánh xạ dữ liệu JSON theo key tùy chỉnh.

Bước 1: Định nghĩa Property Wrapper Serializable

Chúng ta tạo một Property Wrapper có thể lưu trữ giá trị và ánh xạ key tùy chỉnh.

@propertyWrapper
final class Serializable<T> {
    var wrappedValue: T?
    private var key: String?
    
    init(_ key: String? = nil) {
        self.key = key
    }
}
Serializable.swift
  • wrappedValue: Giá trị được lưu trữ, hiện tại chúng ta để nó là generic T.
  • key: Tên key trong JSON, nếu có.

Bước 2: Hỗ trợ encode (mã hóa)

Để Serializable hỗ trợ encode dữ liệu vào JSON, chúng ta cần mở rộng Property Wrapper bằng

protocol XXEncodableKey {
    typealias EncodableContainer = KeyedEncodingContainer<XXCodingKeys>
    func encode(from container: inout EncodableContainer, codingKey: XXCodingKeys) throws
}

extension Serializable: XXEncodableKey where T: Encodable {
    func encode(from container: inout EncodableContainer, codingKey: XXCodingKeys) throws {
        let _codingKey = key.map { XXCodingKeys(stringValue: $0) } ?? codingKey
        try container.encodeIfPresent(wrappedValue, forKey: _codingKey)
    }
}
XXEncodableKey.swift

Giải thích:

  • Chúng ta sẽ mã hóa theo key được lấy từ Property Wrapper Serializable, nếu có.
  • Sử dụng container.encodeIfPresent(…) để mã hóa dữ liệu.

Bước 3: Hỗ trợ decode (giải mã)

Tương tự, ta mở rộng Property Wrapper để hỗ trợ decode từ JSON:

protocol XXDecodableKey {
    typealias DecodableContainer = KeyedDecodingContainer<XXCodingKeys>
    func decode(from container: DecodableContainer, codingKey: XXCodingKeys) throws
}

extension Serializable: XXDecodableKey where T: Decodable {
    func decode(from container: DecodableContainer, codingKey: XXCodingKeys) throws {
        guard wrappedValue == nil else { return }

        let _codingKey = key.map { XXCodingKeys(stringValue: $0) } ?? codingKey
        do {
            wrappedValue = try container.decodeIfPresent(T.self, forKey: _codingKey)
        } catch {
            if let stringValue = try container.decodeIfPresent(String.self, forKey: _codingKey),
               let data = stringValue.data(using: .utf8) {
                wrappedValue = try JSONDecoder().decode(T.self, from: data)
            }
        }
    }
}
XXDecodableKey.swift

Giải thích:

  • Chúng ta thực giải mã theo key được lấy từ từ Property Wrapper Serializable, nếu có.
  • Sử dụng hàm container.decodeIfPresent() để giải mã.
  • Nếu decode thất bại, kiểm tra xem giá trị có phải là một chuỗi JSON và tiếp tục parse nó.

4. Hỗ trợ Encodable và Decodable

Chúng ta định nghĩa một CodingKey và Codable tùy chỉnh giúp chuyển đổi giữa kiểu Swift và JSON:

struct XXCodingKeys: CodingKey {
    var stringValue: String
    var intValue: Int?

    init(intValue: Int) {
        self.intValue = intValue
        self.stringValue = String(intValue)
    }

    init(stringValue: String) {
        self.stringValue = stringValue
    }
}

// Sau đó, chúng ta tạo hai protocol XXEncodable và XXDecodable để ánh xạ tự động các thuộc tính trong một đối tượng Swift:

protocol XXEncodable: Encodable {}
protocol XXDecodable: Decodable {
    init()
}

typealias XXCodable = XXEncodable & XXDecodable

extension XXEncodable {
    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: XXCodingKeys.self)
        
        for child in Mirror(reflecting: self).children {
            guard let encodableKey = child.value as? XXEncodableKey,
                  let label = child.label else {
                continue
            }
            let keyName = label.snakeCaseToCamelCase()
            try encodableKey.encode(from: &container, codingKey: XXCodingKeys(stringValue: keyName))
        }
    }
}

extension XXDecodable {
    init(from decoder: any Decoder) throws {
        self.init()
        let container = try decoder.container(keyedBy: XXCodingKeys.self)
        for child in Mirror(reflecting: self).children {
            guard let decodableKey = child.value as? XXDecodableKey,
                  let label = child.label else {
                continue
            }
            try decodableKey.decode(from: container, codingKey: XXCodingKeys(stringValue: label))
        }
    }
}
XXCodable.py

Giải thích:

  • Ở trong hàm mở rộng của XXEncodableXXDecodable có sử dụng struct Mirror để duyệt qua toàn bộ các variable name của object.
  • Sử dụng XXCodingKeys đỉnh khởi tạo CodingKey với label của object

5. Cách sử dụng Property Wrapper Serializable

Bây giờ, chúng ta áp dụng Property Wrapper vào một mô hình dữ liệu cụ thể:

Bước 1: Khai báo lớp Person

class Person: XXCodable {
    @Serializable("full_name")
    var fullName: String?
    @Serializable
    var district: String?
    
    required init() {}
}
Person.swift
  • Thuộc tính fullName sẽ được ánh xạ với key full_name trong JSON.
  • Bạn để ý biến district trùng với key ở trong json nên chỉ cần khai báo @Serializable

Bước 2: Decode JSON sử dụng JSONDecoder

let json = """
            {
               "full_name": "Nguyen Van A",
               "district": "Vinh Phuc"
            }
         """
do {
   guard let data = json.data(using: .utf8) else {
      return
   }
   let jsonDecoder = JSONDecoder()
   let value = try jsonDecoder.decode(Person.self, from: data)
   print("My name is \(value.fullName ?? "")")
} catch {
   print("error \(error)")
}
ViewController.swift

Kết quả in ra:

Kết quả
Kết quả

6. Lợi ích của Property Wrapper Serializable

  • Tự động ánh xạ key JSON
  • Dễ dàng ánh xạ JSON key sang tên thuộc tính trong Swift.
  • Tách biệt logic encode/decode
  • Định nghĩa encode/decode ngay trong Property Wrapper.
  • Hỗ trợ chuyển đổi định dạng
  • Chuyển đổi chuỗi JSON thành đối tượng Swift một cách linh hoạt.
  • Code gọn gàng, dễ bảo trì
  • Không cần viết nhiều code lặp lại trong mỗi lớp.

7. Kết luận

Property Wrapper giúp chúng ta xử lý encode/decode dữ liệu một cách hiệu quả. Serializable giúp ánh xạ dữ liệu JSON với tên thuộc tính tùy chỉnh, làm cho mã nguồn rõ ràng và dễ bảo trì hơn.

Tóm tắt các bước triển khai:

  • Định nghĩa Property Wrapper Serializable.
  • Viết logic encode/decode dữ liệu từ JSON.
  • Hỗ trợ Encodable và Decodable bằng cách sử dụng XXEncodableXXDecodable.
  • Áp dụng Serializable vào mô hình dữ liệu Person.
  • Decode JSON và kiểm tra kết quả.

Bạn có thể mở rộng Serializable để xử lý các kiểu dữ liệu khác hoặc áp dụng vào những trường hợp cụ thể hơn như UserDefaults, Database, v.v. Hãy thử áp dụng Property Wrapper vào dự án của bạn để thấy sự tiện lợi mà nó mang lại!