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프로토콜 을 사용한다고 생각 하지만 사용 방법을 알 수 없습니다.

답변:


258

코딩 키 수동 사용자 지정

귀하의 예에서는 Codable모든 속성이 Codable. 이 적합성은 단순히 속성 이름에 해당하는 키 유형을 자동으로 생성합니다.이 키 유형은 단일 키 컨테이너에서 인코딩 / 디코딩하는 데 사용됩니다.

그러나이 자동 생성 적합성의 정말 멋진 기능 중 하나 는 프로토콜 을 준수 하는 enum" CodingKeys"(또는 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대한 요구 합성 enums, EncodableDecodable요구 자동 아니라 특정 유형으로 합성 될 수있다 :

  1. 해당 Encodable속성을 모두 준수하는 유형 은 케이스 이름에 Encodable대한 자동 생성 String지원 CodingKey열거 형 매핑 속성을 가져옵니다 . Decodable속성이 모두 인 유형의 경우 유사 합니다.Decodable

  2. (1)에 떨어지는 유형 - 수동으로 제공하고, A 형 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_case대한 자동 JSON 키camelCase

당신이 당신의 이름을 바꾸면 스위프트 4.1 년 zip에 재산을 zipCode, 당신은에 전략을 디코딩 / 키 인코딩을 활용할 수 JSONEncoderJSONDecoder자동으로하기 위해 사이 코딩 키를 변환 camelCase하고 snake_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")

그러나이 전략에 대해 주목해야 할 한 가지 중요한 점은 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 키 매핑

Swift 4.1에서는 및에서 사용자 지정 키 인코딩 / 디코딩 전략을 활용 JSONEncoder하여 JSONDecoder코딩 키를 매핑하는 사용자 지정 함수를 제공 할 수 있습니다.

제공하는 함수 [CodingKey]는 인코딩 / 디코딩의 현재 지점에 대한 코딩 경로를 나타내는를 사용합니다 (대부분의 경우 마지막 요소, 즉 현재 키만 고려하면됩니다). 함수는 CodingKey이 배열의 마지막 키를 대체 할 a 를 반환합니다 .

예를 들어 속성 이름에 UpperCamelCase대한 JSON 키는 lowerCamelCase다음과 같습니다.

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

이걸 직접 우연히 발견했습니다! 변경하려는 키 하나만 무시하고 나머지는 그대로 두는 방법이 있을까요? 예를 들어 case 문에서 CodingKeys열거 형 아래에 있습니다 . 변경중인 키 하나만 나열해도됩니까?
chrismanderson

2
"""A의 인 멀티 라인 : 문자
마틴 R

6
@MartinR 또는 탈출하지 않고, 심지어 단 한 줄의 문자 "들 : D
해미

1
@chrismanderson 정확합니다 – 특히 컴파일러가 케이스 이름이 속성 이름과 동기화되도록 강제하는 경우 ( Codable다른 방법을 따르지 않는다는 오류가 표시됨)
Hamish

1
@ClayEllis 아 예, 물론 중첩 된 컨테이너를 사용하는 경우 예를 들어 이니셜 라이저에서 직접적으로 Address부모 개체 그래프의 특정 위치에서 시작하는 JSON 개체를 디코딩하는 데 불필요하게 연결됩니다. 디코더 자체까지의 시작 키 경로를 추상화하는 것이 훨씬 더 좋을 것입니다. 여기 에 대략적인 hackey-ish 구현이 있습니다.
Hamish

17

Swift 4.2에서는 필요에 따라 다음 3 가지 전략 중 하나를 사용하여 모델 객체 사용자 지정 속성 이름이 JSON 키와 일치하도록 할 수 있습니다.


#1. 사용자 지정 코딩 키 사용

다음 구현 으로 Codable( DecodableEncodable프로토콜) 을 준수하는 구조체를 선언 할 때 ...

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에 스네이크 케이스 키가 있고이를 모델 객체의 낙타 케이스 속성으로 변환하려는 경우 JSONEncoderkeyEncodingStrategyJSONDecoderkeyDecodingStrategy속성을 .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")
 */

#삼. 사용자 지정 키 코딩 전략 사용

필요한 경우 JSONEncoderJSONDecoder사용 키를 코딩 매핑 사용자 정의 전략을 수립 할 수 있도록 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을 당신은 codable 또는 복호 프로토콜에서 사용자 지정 키를 사용할 수 있습니다.

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.