Parse JSON với ObjectMapper

1. Giới thiệu

Ngày nay JSON (JavaScript Object Notation), với các ưu điểm như dễ sử dụng, dễ đọc đã trở nên cực kỳ phổ biến trong việc giao tiếp dữ liệu giữa các web service.

Dưới đây là một ví dụ về 1 dữ liệu JSON:

[
  {
    "product": {
      "name": "Keyboard",
      "price": 12
    }
  },
  {
    "product": {
      "name": "Monitor",
      "price": 200
    }
  }
]

Class model Product tương ứng:

class Product {
  var name = ""
  var price = 0
}

Giả sử ta nhận dữ liệu JSONdata như trên từ server, việc parse dữ liệu với Swift ta phải thực hiện như sau:

var jsonArray: Array!
do {
  jsonArray = try NSJSONSerialization.JSONObjectWithData(JSONData, options: NSJSONReadingOptions()) as? Array
} catch {
  print(error)
}

var products = [Product]()
for json in jsonArray {
  if let item = json as? [String: AnyObject] {
    if let productJson = item["product"] as? [String: AnyObject],
      let name = productJson["name"] as? String,
      let price = productJson["price"] as? Int
      {
        let product = Product()
        product.name = name
        product.price = price
        products.append(product)
    }
  }
}

Như vậy để lấy mỗi đối tượng trong JSON, chúng ta phải kiểm tra xem có bằng nil hay không trước sau đó chuyển về kiểu tương ứng, việc này với các đối tượng phức tạp sẽ phải mất rất nhiều đoạn code kiểm tra và chuyển đổi. Điều này dẫn tới lặp rất nhiều đoạn code và dễ dẫn tới sai lầm, khó tìm ra lỗi.

ObjectMapper là một framework được viết trên Swift nhằm giúp việc chuyển đổi giữa class model và JSON được dễ dàng.

2.ObjectMapper

2.1. Cài đặt

Ta có thể cài thông qua Pod:

pod 'ObjectMapper', '~> 1.3'

2.2. Tính năng

  • Ánh xạ (map) JSON sang model object
  • Map object sang JSON
  • Hỗ trợ các đối tượng con (nested object), bao gồm các đối tượng lẻ hay trong mảng, từ điển...
  • Tùy biến việc chuyển đổi kiểu dữ liệu trong quá trình map
  • Hỗ trợ struct

2.3. Map dữ liệu

Để hỗ trợ map, class/struct cần phải thực thi protocol Mappable gồm hai hàm sau:

init?(_ map: Map)
mutating func mapping(map: Map)

Ta sửa lại class Product như sau:

class Product: Mappable {
  var name = ""
  var price = 0

  init() {}

  required init?(_ map: Map)()

  func mapping(map: Map) {
    name <- map["name"]
    price <- map["price"]
  }
}

Đoạn code map JSON được viết lại đơn giản như sau:

var jsonArray: Array!
do {
  jsonArray = try NSJSONSerialization.JSONObjectWithData(JSONData, options: NSJSONReadingOptions()) as? Array
} catch {
  print(error)
}

var products = [Product]()
for json in jsonArray {
  if let item = json as? [String: AnyObject] {
    if let productJson = item["product"] as? [String: AnyObject],
      product = Product(JSON: productJson)
      {
        products.append(product)
    }
  }
}

Như vậy chúng ta có thể bỏ qua các đoạn code check và parse dữ liệu của các trường đối tượng, thay vào đó chúng ta chỉ cần đưa tham số kiểu dictionary vào hàm khởi tạo và ObjectMapper sẽ thực hiện việc map một cách tự động, các trường bị thiếu sẽ bỏ qua và để giá trị mặc định:

Product(JSON: productJson)

2.4. Các kiểu được hỗ trợ bởi ObjectMapper

  • Int
  • Bool
  • Double
  • Float
  • String
  • RawRepresentable (Enums)
  • Array<AnyObject>
  • Dictionary<String, AnyObject>
  • Object<T: Mappable>
  • Array<T: Mappable>
  • Array<Array<T: Mappable>>
  • Set<T: Mappable>
  • Dictionary<String, T: Mappable>
  • Dictionary<String, Array<T: Mappable>>

2.5. Mappable Protocol

mutating func mapping(map: Map)

Hàm mapping chứa các khai báo về map dữ liệu và sẽ được gọi sau khi đối tượng được tạo.

init?(_ map: Map)

Hàm khởi tạo được sử dụng bởi ObjectMapper để khởi tạo đối tượng. Nó cũng có thể được sử dụng bởi lập trình viên để kiểm tra JSON trước khi khởi tạo đối tượng.

required init?(_ map: Map){
    if map.JSONDictionary["name"] == nil {
        return nil
    }
}

2.6. Map đối tượng con bên trong (nested object)

Giả sử ta có đối tượng JSON như sau:

"distance" : {
     "text" : "102 ft",
     "value" : 31
}

Ta có thể map đối tượng distance nhận giá trị value như sau:

func mapping(map: Map) {
    distance <- map["distance.value"]
}

Truy cập phần tử trong mảng:

distance <- map["distances.0.value"]

2.7. Tùy biến chuyển kiểu dữ liệu

ObjectMapper hỗ trợ việc chuyển các kiểu dữ liệu khác nhau, ví dụ như chuyển kiểu dữ liệu Int sang NSDate như dưới đây:

birthday <- (map["birthday"], DateTransform())

Ta có thể tùy biến việc chuyển đổi thông qua việc tạo các class kế thừa protocol TransformType:

public protocol TransformType {
    associatedtype Object
    associatedtype JSON

    func transformFromJSON(_ value: Any?) -> Object?
    func transformToJSON(_ value: Object?) -> JSON?
}

TransformOf

Ngoài việc tạo class như trên, ta có thể sử dụng class TransformOf để chuyển đổi kiểu dữ liệu. TransformOf là một class generic, được khởi tạo với đầu vào là 2 kiểu dữ liệu và 2 closure. Ví dụ dưới đây cho phép chuyển đổi dữ liệu kiểu String và Int:

let transform = TransformOf<Int, String>(fromJSON: { (value: String?) -> Int? in
    // transform value from String? to Int?
    return Int(value!)
}, toJSON: { (value: Int?) -> String? in
    // transform value from Int? to String?
    if let value = value {
        return String(value)
    }
    return nil
})

id <- (map["id"], transform)

2.8 Mapping Context

Trong nhiều trường hợp ta cần truyền thêm dữ liệu vào quá trình mapping, ta có thể khai báo kiểu kế thừa MapContext:

struct Context: MapContext {
    var importantMappingInfo = "Info that I need during mapping"
}

class User: Mappable {
    var name: String?

    required init?(_ map: Map){

    }

    func mapping(map: Map){
        if let context = map.context as? Context {
            // use context to make decisions about mapping
        }
    }
}

let context = Context()
let user = Mapper<User>(context: context).map(JSONString)

3. Kết luận

Trên đây tôi đã giới thiệu cơ bản về ObjectMapper và các áp dụng của nó, các bạn có thể tìm hiểu kỹ hơn qua trang git: https://github.com/Hearst-DD/ObjectMapper

All Rights Reserved