Làm cách nào để sử dụng các khóa tùy chỉnh với giao thức Có thể giải mã của Swift 4?


102

Swift 4 đã giới thiệu hỗ trợ mã hóa và giải mã JSON thông qua Decodablegiao thức. Làm cách nào để sử dụng các khóa tùy chỉnh cho việc này?

Ví dụ: giả sử tôi có một cấu trúc

struct Address:Codable {
    var street:String
    var zip:String
    var city:String
    var state:String
}

Tôi có thể mã hóa nó thành JSON.

let address = Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")

if let encoded = try? encoder.encode(address) {
    if let json = String(data: encoded, encoding: .utf8) {
        // Print JSON String
        print(json)

        // JSON string is 
           { "state":"California", 
             "street":"Apple Bay Street", 
             "zip":"94608", 
             "city":"Emeryville" 
           }
    }
}

Tôi có thể mã hóa điều này trở lại một đối tượng.

    let newAddress: Address = try decoder.decode(Address.self, from: encoded)

Nhưng nếu tôi có một đối tượng json thì

{ 
   "state":"California", 
   "street":"Apple Bay Street", 
   "zip_code":"94608", 
   "city":"Emeryville" 
}

Làm cách nào để tôi nói với người giải mã trên bản đồ Addressđó ? Tôi tin rằng bạn sử dụng giao thức mới , nhưng tôi không thể tìm ra cách sử dụng giao thức này.zip_codezipCodingKey

Câu trả lời:


257

Tùy chỉnh thủ công các khóa mã hóa

Trong ví dụ của bạn, bạn đang nhận được một quy tắc được tạo tự động Codablevì tất cả các thuộc tính của bạn cũng tuân theo Codable. Sự tuân thủ này tự động tạo ra một loại khóa đơn giản tương ứng với tên thuộc tính - sau đó được sử dụng để mã hóa / giải mã từ một vùng chứa có khóa duy nhất.

Tuy nhiên một thực sự tính năng gọn gàng của sự phù hợp tự động tạo ra này là nếu bạn định nghĩa một lồng enumtrong loại của bạn được gọi là " CodingKeys" (hoặc sử dụng một typealiasvới tên này) mà phù hợp với những CodingKeygiao thức - Swift sẽ tự động sử dụng này làm loại khóa. Do đó, điều này cho phép bạn dễ dàng tùy chỉnh các khóa mà thuộc tính của bạn được mã hóa / giải mã.

Vì vậy, điều này có nghĩa là bạn chỉ có thể nói:

struct Address : Codable {

    var street: String
    var zip: String
    var city: String
    var state: String

    private enum CodingKeys : String, CodingKey {
        case street, zip = "zip_code", city, state
    }
}

Tên trường hợp enum cần phải khớp với tên thuộc tính và giá trị thô của những trường hợp này cần khớp với các khóa mà bạn đang mã hóa / giải mã từ đó (trừ khi được chỉ định khác, các giá trị thô của kiểu Stringliệt kê sẽ giống như tên trường hợp ). Do đó, thuộc ziptính bây giờ sẽ được mã hóa / giải mã bằng cách sử dụng khóa "zip_code".

Các quy tắc chính xác cho sự tuân thủ Encodable/ được tạo tự động Decodableđược nêu chi tiết bởi đề xuất phát triển (tôi nhấn mạnh):

Ngoài việc CodingKeytổng hợp yêu cầu tự động cho enums, Encodable& Decodablecác yêu cầu cũng có thể được tổng hợp tự động cho một số loại nhất định:

  1. Các kiểu tuân theo Encodablethuộc tính của chúng đều Encodablenhận được thuộc tính ánh xạ enum- Stringbacked được tạo tự động CodingKeyvới tên trường hợp. Tương tự đối với Decodablecác loại có thuộc tính làDecodable

  2. Các loại thuộc (1) - và các loại cung cấp thủ công CodingKey enum(được đặt tên CodingKeys, trực tiếp hoặc thông qua a typealias) có các trường hợp ánh xạ từ 1 đến 1 đến Encodable/ Decodablethuộc tính theo tên - nhận tổng hợp tự động init(from:)encode(to:)khi thích hợp, sử dụng các thuộc tính và khóa đó

  3. Các loại không thuộc (1) và (2) sẽ phải cung cấp loại khóa tùy chỉnh nếu cần và cung cấp loại khóa riêng init(from:)encode(to:)nếu thích hợp

Mã hóa mẫu:

import Foundation

let address = Address(street: "Apple Bay Street", zip: "94608",
                      city: "Emeryville", state: "California")

do {
    let encoded = try JSONEncoder().encode(address)
    print(String(decoding: encoded, as: UTF8.self))
} catch {
    print(error)
}
//{"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}

Giải mã ví dụ:

// using the """ multi-line string literal here, as introduced in SE-0168,
// to avoid escaping the quotation marks
let jsonString = """
{"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}
"""

do {
    let decoded = try JSONDecoder().decode(Address.self, from: Data(jsonString.utf8))
    print(decoded)
} catch {
    print(error)
}

// Address(street: "Apple Bay Street", zip: "94608",
// city: "Emeryville", state: "California")

Các snake_casekhóa JSON tự động cho camelCasetên thuộc tính

Trong Swift 4.1, nếu bạn đổi tên thuộc tính của mình zipthành zipCode, bạn có thể tận dụng các chiến lược mã hóa / giải mã khóa trênJSONEncoderJSONDecoderđể tự động chuyển đổi khóa mã hóa giữa camelCasesnake_case.

Mã hóa mẫu:

import Foundation

struct Address : Codable {
  var street: String
  var zipCode: String
  var city: String
  var state: String
}

let address = Address(street: "Apple Bay Street", zipCode: "94608",
                      city: "Emeryville", state: "California")

do {
  let encoder = JSONEncoder()
  encoder.keyEncodingStrategy = .convertToSnakeCase
  let encoded = try encoder.encode(address)
  print(String(decoding: encoded, as: UTF8.self))
} catch {
  print(error)
}
//{"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}

Giải mã ví dụ:

let jsonString = """
{"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}
"""

do {
  let decoder = JSONDecoder()
  decoder.keyDecodingStrategy = .convertFromSnakeCase
  let decoded = try decoder.decode(Address.self, from: Data(jsonString.utf8))
  print(decoded)
} catch {
  print(error)
}

// Address(street: "Apple Bay Street", zipCode: "94608",
// city: "Emeryville", state: "California")

Tuy nhiên, một điều quan trọng cần lưu ý về chiến lược này là nó sẽ không thể đảo ngược một số tên thuộc tính bằng các từ viết tắt hoặc chữ viết tắt, theo nguyên tắc thiết kế API Swift , phải là chữ hoa hoặc chữ thường (tùy thuộc vào vị trí ).

Ví dụ: một thuộc tính có tên someURL sẽ được mã hóa bằng khóa some_url, nhưng khi giải mã, thuộc tính này sẽ được chuyển thành someUrl.

Để khắc phục điều này, bạn sẽ phải chỉ định thủ công khóa mã hóa cho thuộc tính đó thành chuỗi mà bộ giải mã mong đợi, ví dụ: someUrltrong trường hợp này (sẽ vẫn được some_urlbộ mã hóa chuyển đổi thành ):

struct S : Codable {

  private enum CodingKeys : String, CodingKey {
    case someURL = "someUrl", someOtherProperty
  }

  var someURL: String
  var someOtherProperty: String
}

(Điều này không trả lời chính xác câu hỏi cụ thể của bạn, nhưng với bản chất chính tắc của Câu hỏi và Đáp này, tôi cảm thấy nó đáng để bao gồm)

Ánh xạ khóa JSON tự động tùy chỉnh

Trong Swift 4.1, bạn có thể tận dụng các chiến lược mã hóa / giải mã khóa tùy chỉnh trên JSONEncoderJSONDecoder , cho phép bạn cung cấp một chức năng tùy chỉnh để ánh xạ các khóa mã hóa.

Hàm bạn cung cấp nhận một [CodingKey], đại diện cho đường dẫn mã hóa cho điểm hiện tại trong mã hóa / giải mã (trong hầu hết các trường hợp, bạn sẽ chỉ cần xem xét phần tử cuối cùng; nghĩa là khóa hiện tại). Hàm trả về a CodingKeysẽ thay thế khóa cuối cùng trong mảng này.

Ví dụ: UpperCamelCasecác khóa JSON cho lowerCamelCasetên thuộc tính:

import Foundation

// wrapper to allow us to substitute our mapped string keys.
struct AnyCodingKey : CodingKey {

  var stringValue: String
  var intValue: Int?

  init(_ base: CodingKey) {
    self.init(stringValue: base.stringValue, intValue: base.intValue)
  }

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

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

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

extension JSONEncoder.KeyEncodingStrategy {

  static var convertToUpperCamelCase: JSONEncoder.KeyEncodingStrategy {
    return .custom { codingKeys in

      var key = AnyCodingKey(codingKeys.last!)

      // uppercase first letter
      if let firstChar = key.stringValue.first {
        let i = key.stringValue.startIndex
        key.stringValue.replaceSubrange(
          i ... i, with: String(firstChar).uppercased()
        )
      }
      return key
    }
  }
}

extension JSONDecoder.KeyDecodingStrategy {

  static var convertFromUpperCamelCase: JSONDecoder.KeyDecodingStrategy {
    return .custom { codingKeys in

      var key = AnyCodingKey(codingKeys.last!)

      // lowercase first letter
      if let firstChar = key.stringValue.first {
        let i = key.stringValue.startIndex
        key.stringValue.replaceSubrange(
          i ... i, with: String(firstChar).lowercased()
        )
      }
      return key
    }
  }
}

Bây giờ bạn có thể mã hóa bằng .convertToUpperCamelCasechiến lược chính:

let address = Address(street: "Apple Bay Street", zipCode: "94608",
                      city: "Emeryville", state: "California")

do {
  let encoder = JSONEncoder()
  encoder.keyEncodingStrategy = .convertToUpperCamelCase
  let encoded = try encoder.encode(address)
  print(String(decoding: encoded, as: UTF8.self))
} catch {
  print(error)
}
//{"Street":"Apple Bay Street","City":"Emeryville","State":"California","ZipCode":"94608"}

và giải mã bằng .convertFromUpperCamelCasechiến lược chính:

let jsonString = """
{"Street":"Apple Bay Street","City":"Emeryville","State":"California","ZipCode":"94608"}
"""

do {
  let decoder = JSONDecoder()
  decoder.keyDecodingStrategy = .convertFromUpperCamelCase
  let decoded = try decoder.decode(Address.self, from: Data(jsonString.utf8))
  print(decoded)
} catch {
  print(error)
}

// Address(street: "Apple Bay Street", zipCode: "94608",
// city: "Emeryville", state: "California")

Chỉ vấp phải điều này bản thân mình! Tôi tự hỏi, có cách nào để chỉ ghi đè một phím mà tôi muốn thay đổi và để yên phần còn lại không? Ví dụ: trong câu lệnh trường hợp, dưới CodingKeysenum; Tôi có thể liệt kê một khóa mà tôi đang thay đổi không?
chrismanderson

2
"""là nghĩa đen nhiều dòng :)
Martin R

6
@MartinR Hoặc thậm chí chỉ là một dòng chữ duy nhất mà không cần phải thoát ra ": D
Hamish

1
@chrismanderson Chính xác - nhất là khi thực thi biên dịch rằng tên trường hợp được lưu giữ trong đồng bộ với tên thuộc tính (nó sẽ cung cấp cho bạn một lỗi nói rằng bạn không phù hợp với Codablecách khác)
Hamish

1
@ClayEllis À vâng, mặc dù tất nhiên việc sử dụng các vùng chứa lồng nhau, chẳng hạn trực tiếp trong trình khởi tạo của các Addressliên kết không cần thiết để giải mã một đối tượng JSON bắt đầu từ một vị trí cụ thể trong biểu đồ đối tượng mẹ. Sẽ đẹp hơn nhiều nếu trừu tượng hóa đường dẫn khóa bắt đầu đến chính bộ giải mã - đây là cách triển khai hackey-ish sơ sài .
Hamish

17

Với Swift 4.2, theo nhu cầu của bạn, bạn có thể sử dụng một trong 3 chiến lược sau để làm cho tên thuộc tính tùy chỉnh của đối tượng mô hình khớp với các khóa JSON của bạn.


# 1. Sử dụng khóa mã hóa tùy chỉnh

Khi bạn khai báo một cấu trúc tuân theo Codable( Decodablevà các Encodablegiao thức) với cách triển khai sau ...

struct Address: Codable {
    var street: String
    var zip: String
    var city: String
    var state: String        
}

... trình biên dịch tự động tạo một enum lồng nhau phù hợp với CodingKeygiao thức cho bạn.

struct Address: Codable {
    var street: String
    var zip: String
    var city: String
    var state: String

    // compiler generated
    private enum CodingKeys: String, CodingKey {
        case street
        case zip
        case city
        case state
    }
}

Do đó, nếu các khóa được sử dụng trong định dạng dữ liệu được tuần tự hóa của bạn không khớp với tên thuộc tính từ kiểu dữ liệu của bạn, bạn có thể triển khai enum này theo cách thủ công và đặt rawValue cho các trường hợp bắt buộc.

Ví dụ sau cho thấy cách thực hiện:

import Foundation

struct Address: Codable {
    var street: String
    var zip: String
    var city: String
    var state: String

    private enum CodingKeys: String, CodingKey {
        case street
        case zip = "zip_code"
        case city
        case state
    }
}

Mã hóa (thay thế thuộc ziptính bằng khóa JSON "zip_code"):

let address = Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")

let encoder = JSONEncoder()
if let jsonData = try? encoder.encode(address), let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)
}

/*
 prints:
 {"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}
 */

Giải mã (thay thế khóa JSON "zip_code" bằng thuộc ziptính):

let jsonString = """
{"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}
"""

let decoder = JSONDecoder()
if let jsonData = jsonString.data(using: .utf8), let address = try? decoder.decode(Address.self, from: jsonData) {
    print(address)
}

/*
 prints:
 Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")
 */

# 2. Sử dụng trường hợp rắn để chiến lược mã hóa phím trường hợp lạc đà

Nếu JSON của bạn có các phím rắn cased và bạn muốn chuyển đổi chúng sang bất động sản lạc đà-cased cho đối tượng mô hình của bạn, bạn có thể thiết lập của bạn JSONEncoder's keyEncodingStrategyJSONDecoder' s keyDecodingStrategytính đến .convertToSnakeCase.

Ví dụ sau cho thấy cách thực hiện:

import Foundation

struct Address: Codable {
    var street: String
    var zipCode: String
    var cityName: String
    var state: String
}

Mã hóa (chuyển đổi các thuộc tính có vỏ lạc đà thành các khóa JSON có vỏ rắn):

let address = Address(street: "Apple Bay Street", zipCode: "94608", cityName: "Emeryville", state: "California")

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
if let jsonData = try? encoder.encode(address), let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)
}

/*
 prints:
 {"state":"California","street":"Apple Bay Street","zip_code":"94608","city_name":"Emeryville"}
 */

Giải mã (chuyển đổi các khóa JSON có vỏ rắn thành các thuộc tính có vỏ bọc lạc đà):

let jsonString = """
{"state":"California","street":"Apple Bay Street","zip_code":"94608","city_name":"Emeryville"}
"""

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
if let jsonData = jsonString.data(using: .utf8), let address = try? decoder.decode(Address.self, from: jsonData) {
    print(address)
}

/*
 prints:
 Address(street: "Apple Bay Street", zipCode: "94608", cityName: "Emeryville", state: "California")
 */

# 3. Sử dụng các chiến lược mã hóa khóa tùy chỉnh

Nếu cần, JSONEncoderJSONDecodercho phép bạn đặt chiến lược tùy chỉnh để ánh xạ các khóa mã hóa bằng cách sử dụng JSONEncoder.KeyEncodingStrategy.custom(_:)JSONDecoder.KeyDecodingStrategy.custom(_:).

Ví dụ sau đây cho thấy cách triển khai chúng:

import Foundation

struct Address: Codable {
    var street: String
    var zip: String
    var city: String
    var state: String
}

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

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

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

Mã hóa (chuyển đổi các thuộc tính chữ cái đầu tiên viết thường thành các khóa JSON chữ cái đầu tiên viết hoa):

let address = Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .custom({ (keys) -> CodingKey in
    let lastKey = keys.last!
    guard lastKey.intValue == nil else { return lastKey }
    let stringValue = lastKey.stringValue.prefix(1).uppercased() + lastKey.stringValue.dropFirst()
    return AnyKey(stringValue: stringValue)!
})

if let jsonData = try? encoder.encode(address), let jsonString = String(data: jsonData, encoding: .utf8) {
    print(jsonString)
}

/*
 prints:
 {"Zip":"94608","Street":"Apple Bay Street","City":"Emeryville","State":"California"}
 */

Giải mã (chuyển đổi các khóa JSON chữ cái đầu tiên viết hoa thành các thuộc tính chữ cái đầu tiên viết thường):

let jsonString = """
{"State":"California","Street":"Apple Bay Street","Zip":"94608","City":"Emeryville"}
"""

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ (keys) -> CodingKey in
    let lastKey = keys.last!
    guard lastKey.intValue == nil else { return lastKey }
    let stringValue = lastKey.stringValue.prefix(1).lowercased() + lastKey.stringValue.dropFirst()
    return AnyKey(stringValue: stringValue)!
})

if let jsonData = jsonString.data(using: .utf8), let address = try? decoder.decode(Address.self, from: jsonData) {
    print(address)
}

/*
 prints:
 Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")
 */

Nguồn:


3

Những gì tôi đã làm là tạo cấu trúc riêng giống như những gì bạn đang nhận được từ JSON đối với các kiểu dữ liệu của nó.

Chỉ như thế này:

struct Track {
let id : Int
let contributingArtistNames:String
let name : String
let albumName :String
let copyrightP:String
let copyrightC:String
let playlistCount:Int
let trackPopularity:Int
let playlistFollowerCount:Int
let artistFollowerCount : Int
let label : String
}

Sau đó, bạn cần tạo một phần mở rộng có cùng phần structmở rộng decodableenumcó cùng cấu trúc CodingKeyvà sau đó bạn cần khởi tạo bộ giải mã bằng cách sử dụng enum này với các khóa và kiểu dữ liệu của nó (Các khóa sẽ đến từ enum và các kiểu dữ liệu sẽ đến hoặc nói được tham chiếu từ chính cấu trúc)

extension Track: Decodable {

    enum TrackCodingKeys: String, CodingKey {
        case id = "id"
        case contributingArtistNames = "primaryArtistsNames"
        case spotifyId = "spotifyId"
        case name = "name"
        case albumName = "albumName"
        case albumImageUrl = "albumImageUrl"
        case copyrightP = "copyrightP"
        case copyrightC = "copyrightC"
        case playlistCount = "playlistCount"
        case trackPopularity = "trackPopularity"
        case playlistFollowerCount = "playlistFollowerCount"
        case artistFollowerCount = "artistFollowers"
        case label = "label"
    }
    init(from decoder: Decoder) throws {
        let trackContainer = try decoder.container(keyedBy: TrackCodingKeys.self)
        if trackContainer.contains(.id){
            id = try trackContainer.decode(Int.self, forKey: .id)
        }else{
            id = 0
        }
        if trackContainer.contains(.contributingArtistNames){
            contributingArtistNames = try trackContainer.decode(String.self, forKey: .contributingArtistNames)
        }else{
            contributingArtistNames = ""
        }
        if trackContainer.contains(.spotifyId){
            spotifyId = try trackContainer.decode(String.self, forKey: .spotifyId)
        }else{
            spotifyId = ""
        }
        if trackContainer.contains(.name){
            name = try trackContainer.decode(String.self, forKey: .name)
        }else{
            name = ""
        }
        if trackContainer.contains(.albumName){
            albumName = try trackContainer.decode(String.self, forKey: .albumName)
        }else{
            albumName = ""
        }
        if trackContainer.contains(.albumImageUrl){
            albumImageUrl = try trackContainer.decode(String.self, forKey: .albumImageUrl)
        }else{
            albumImageUrl = ""
        }
        if trackContainer.contains(.copyrightP){
            copyrightP = try trackContainer.decode(String.self, forKey: .copyrightP)
        }else{
            copyrightP = ""
        }
        if trackContainer.contains(.copyrightC){
                copyrightC = try trackContainer.decode(String.self, forKey: .copyrightC)
        }else{
            copyrightC = ""
        }
        if trackContainer.contains(.playlistCount){
            playlistCount = try trackContainer.decode(Int.self, forKey: .playlistCount)
        }else{
            playlistCount = 0
        }

        if trackContainer.contains(.trackPopularity){
            trackPopularity = try trackContainer.decode(Int.self, forKey: .trackPopularity)
        }else{
            trackPopularity = 0
        }
        if trackContainer.contains(.playlistFollowerCount){
            playlistFollowerCount = try trackContainer.decode(Int.self, forKey: .playlistFollowerCount)
        }else{
            playlistFollowerCount = 0
        }

        if trackContainer.contains(.artistFollowerCount){
            artistFollowerCount = try trackContainer.decode(Int.self, forKey: .artistFollowerCount)
        }else{
            artistFollowerCount = 0
        }
        if trackContainer.contains(.label){
            label = try trackContainer.decode(String.self, forKey: .label)
        }else{
            label = ""
        }
    }
}

Bạn cần thay đổi ở đây từng khóa và kiểu dữ liệu theo nhu cầu của mình và sử dụng nó với bộ giải mã.


-1

Bằng cách sử dụng CodingKey, bạn có thể sử dụng các khóa tùy chỉnh trong giao thức có thể mã hóa hoặc giải mã.

struct person: Codable {
    var name: String
    var age: Int
    var street: String
    var state: String

    private enum CodingKeys: String, CodingKey {
        case name
        case age
        case street = "Street_name"
        case state
    } }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.