Swift 4のDecodableプロトコルでカスタムキーを使用するにはどうすればよいですか?


102

Swift 4では、Decodableプロトコルを介したネイティブJSONエンコーディングおよびデコーディングのサポートが導入されました。これにカスタムキーを使用するにはどうすればよいですか?

たとえば、構造体があるとしましょう

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

これを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" 
           }
    }
}

これをエンコードしてオブジェクトに戻すことができます。

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

しかし、私が持っていたjsonオブジェクトがあった場合

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

Addressそのzip_codeマップのデコーダーにどのように伝えzipますか?新しいCodingKeyプロトコルを使用していると思いますが、使用方法がわかりません。

回答:


257

コーディングキーの手動カスタマイズ

あなたの例では、Codableすべてのプロパティもに準拠しているため、自動生成された準拠を取得していますCodable。この準拠により、プロパティ名に単に対応するキータイプが自動的に作成されます。これは、単一のキー付きコンテナーへのエンコード/デコードを行うために使用されます。

ただし、この自動生成された適合の本当に優れた機能の1つ、プロトコルに準拠したenumCodingKeys」と呼ばれるタイプにネストを定義する(またはtypealiasこの名前でを使用する)と、CodingKeySwiftはこれをキータイプとして自動的に使用します。したがって、これにより、プロパティのエンコード/デコードに使用するキーを簡単にカスタマイズできます。

つまり、これはあなたがただ言うことができるということです:

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
    }
}

列挙型のケース名はプロパティ名と一致する必要があり、これらのケースの未加工の値は、エンコード先またはデコード元のキーと一致する必要があります(特に指定しない限り、String列挙型の未加工の値はケース名と同じになります) )。したがって、zipプロパティはキーを使用してエンコード/デコードされます"zip_code"

自動生成されたEncodable/ Decodable適合の正確なルールは、進化の提案(鉱山の強調)で詳しく説明されています。

自動に加えCodingKeyの要件合成 enumsEncodableおよびDecodable要件も自動的に特定の種類のために合成することができます。

  1. Encodableプロパティがすべて一致するタイプには、プロパティがケース名にマッピングEncodableStringれたCodingKey列挙型マッピングが自動的に生成されます。同様にDecodable、プロパティがすべてであるタイプの場合Decodable

  2. (1)に落ちるタイプ- 手動で提供し、タイプCodingKey enum(名前CodingKeys、直接、またはを介してtypealias)、その場合1対1のマッピングEncodable/ Decodable名前によって特性が -の自動合成取得init(from:)し、encode(to:)それらの特性およびキーを使用して、適切なように

  3. 陥るタイプでもない(1)や(2)必要に応じてカスタムキーの種類を提供し、独自に提供する必要がありますinit(from:)し、 encode(to:)必要に応じて、

エンコーディングの例:

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"}

デコードの例:

// 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")

プロパティ名の自動snake_caseJSONキーcamelCase

Swift 4.1では、zipプロパティの名前をに変更すると、およびの間で自動的にコーディングキーを変換するためにzipCodeJSONEncoderおよびJSONDecoderでキーのエンコード/デコード戦略を利用できます。camelCasesnake_case

エンコーディングの例:

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"}

デコードの例:

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")

ただし、この戦略について注意すべき重要な点の1つは、Swift API設計ガイドラインによれば、大文字と小文字を同じにする必要がある頭字語や頭文字で一部のプロパティ名をラウンドトリップできないことです(位置によって異なります)。 )。

たとえば、という名前のプロパティsomeURLはキーsome_urlでエンコードされますが、デコードすると、に変換されsomeUrlます。

この問題を解決するには、手動で例えば、文字列デコーダ期待していること、であることをそのプロパティのコーディングキーを指定する必要がありますsomeUrl(まだに変換されます。この場合、some_urlエンコーダによって):

struct S : Codable {

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

  var someURL: String
  var someOtherProperty: String
}

(これは厳密にあなたの特定の質問に答えるものではありませんが、このQ&Aの標準的な性質を考えると、含める価値はあると思います)

カスタムの自動JSONキーマッピング

スウィフト4.1では、カスタムキーに戦略を符号化/復号化を利用することができますJSONEncoderJSONDecoder、あなたがコーディングのキーをマップするカスタム機能を提供することができます。

提供する関数はを受け取ります[CodingKey]。これは、エンコード/デコードの現在のポイントのコーディングパスを表します(ほとんどの場合、最後の要素、つまり現在のキーのみを考慮する必要があります)。関数は、CodingKeyこの配列の最後のキーを置き換えるa を返します。

たとえばUpperCamelCaselowerCamelCaseプロパティ名のJSONキー:

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
    }
  }
}

これで、.convertToUpperCamelCaseキー戦略でエンコードできます。

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"}

そして.convertFromUpperCamelCase重要な戦略でデコードします:

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")

自分でこれにつまずいただけです!変更したい1つのキーのみをオーバーライドし、残りをそのままにする方法はあるのでしょうか。たとえば、CodingKeys列挙型の下のケースステートメント。変更する1つのキーのみを一覧表示できますか?
chrismanderson 2017

2
"""用であるマルチラインリテラル:)
マーティンR

6
@MartinRまたは、"s をエスケープする必要のない単一行リテラル:D
Hamish

1
@chrismanderson正確–特にコンパイラがケース名とプロパティ名の同期を維持するように強制している場合(Codableそうでない場合は準拠していないというエラーが表示されます)
Hamish

1
@ClayEllisああそうです。もちろん、たとえばネストされたコンテナをイニシャライザで直接使用するAddressと、親オブジェクトグラフの特定の場所で始まるJSONオブジェクトをデコードする必要がなくなります。デコーダ自体に至るまでの開始キーパスを抽象化すると、はるかに優れた方法になります。これは、大まかなハッキーっぽい実装です。
ハミッシュ

17

Swift 4.2では、必要に応じて、モデルオブジェクトのカスタムプロパティ名をJSONキーに一致させるために、次の3つの戦略のいずれかを使用できます。


#1。カスタムコーディングキーの使用

次の実装でCodableDecodableおよびEncodableプロトコル)に準拠する構造体を宣言すると...

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

...コンパイラは、CodingKeyプロトコルに準拠するネストされた列挙型を自動的に生成します。

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
    }
}

したがって、シリアル化されたデータ形式で使用されるキーがデータ型のプロパティ名と一致しない場合は、この列挙型を手動で実装rawValueし、必要なケースに適切な値を設定できます。

次の例は、その方法を示しています。

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
    }
}

エンコード(zipプロパティを「zip_code」JSONキーで置き換える):

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"}
 */

デコード(「zip_code」JSONキーをzipプロパティで置き換える):

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。ヘビケースからラクダケースへのキーコーディング戦略

あなたのJSONはヘビケースに入れたキーを持って、あなたがあなたのモデルオブジェクトのためのキャメルケースのプロパティに変換したい場合は、あなたの設定することができますJSONEncoder「S keyEncodingStrategyJSONDecoderS」keyDecodingStrategyにプロパティを.convertToSnakeCase

次の例は、その方法を示しています。

import Foundation

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

エンコード(キャメルケースのプロパティをヘビケースのJSONキーに変換):

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"}
 */

デコード(ヘビのケースのJSONキーをキャメルのケースのプロパティに変換):

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。カスタムキーコーディング戦略の使用

必要に応じて、JSONEncoderそしてJSONDecoderあなたが使用してコーディングキーをマップするカスタム戦略を設定することが可能JSONEncoder.KeyEncodingStrategy.custom(_:)JSONDecoder.KeyDecodingStrategy.custom(_:)

次の例は、それらを実装する方法を示しています。

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
    }
}

エンコード(小文字の最初の文字のプロパティを大文字の最初の文字のJSONキーに変換):

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"}
 */

デコード(大文字の最初の文字のJSONキーを小文字の最初の文字のプロパティに変換):

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")
 */

出典:


3

私が行ったことは、データ型に関してJSONから得ているのと同じように独自の構造を作成することです。

ちょうどこのような:

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
}

この後、同じstruct拡張decodableenum同じ構造の拡張を作成する必要があります。CodingKey次に、この列挙型とそのキーおよびデータ型を使用してデコーダを初期化する必要があります(キーは列挙型から取得され、データ型は来ているか言う構造自体から参照される)

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 = ""
        }
    }
}

ここでは、必要に応じてすべてのキーとデータ型を変更し、デコーダーで使用する必要があります。


-1

CodingKeyを使用することにより、コード化可能またはデコード可能なプロトコルでカスタムキーを使用できます。

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
    } }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.