How do I use custom keys with Swift 4's Decodable protocol?

前端 未结 4 870
面向向阳花
面向向阳花 2020-11-22 14:57

Swift 4 introduced support for native JSON encoding and decoding via the Decodable protocol. How do I use custom keys for this?

E.g., say I have a struct

<         


        
相关标签:
4条回答
  • 2020-11-22 15:46

    What I have done is create own structure just like what you are getting from the JSON with respect to its data types.

    Just like this:

    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
    }
    

    After this you need to create an extension of the same struct extending decodable and the enum of the same structure with CodingKey and then you need to initialize the decoder using this enum with its keys and datatypes (Keys will come from the enum and the datatypes will be coming or say referenced from the structure itself)

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

    You need to change here each and every key and datatypes according to your needs and use it with the decoder.

    0 讨论(0)
  • 2020-11-22 15:47

    By using CodingKey you can use custom keys in codable or decodable protocol.

    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
        } }
    
    0 讨论(0)
  • 2020-11-22 15:53

    With Swift 4.2, according to your needs, you may use one of the 3 following strategies in order to make your model objects custom property names match your JSON keys.


    #1. Using custom coding keys

    When you declare a struct that conforms to Codable (Decodable and Encodable protocols) with the following implementation...

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

    ... the compiler automatically generates a nested enum that conforms to CodingKey protocol for you.

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

    Therefore, if the keys used in your serialized data format don't match the property names from your data type, you can manually implement this enum and set the appropriate rawValue for the required cases.

    The following example shows how to do:

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

    Encode (replacing zip property with "zip_code" JSON key):

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

    Decode (replacing "zip_code" JSON key with zip property):

    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. Using snake case to camel case key coding strategies

    If your JSON has snake-cased keys and you want to convert them to camel-cased properties for your model object, you can set your JSONEncoder's keyEncodingStrategy and JSONDecoder's keyDecodingStrategy properties to .convertToSnakeCase.

    The following example shows how to do:

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

    Encode (converting camel cased properties into snake cased JSON keys):

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

    Decode (converting snake cased JSON keys into camel cased properties):

    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. Using custom key coding strategies

    If necessary, JSONEncoder and JSONDecoder allow you to set a custom strategy to map coding keys using JSONEncoder.KeyEncodingStrategy.custom(_:) and JSONDecoder.KeyDecodingStrategy.custom(_:).

    The following example shows how to implement them:

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

    Encode (converting lowercased first letter properties into uppercased first letter JSON keys):

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

    Decode (converting uppercased first letter JSON keys into lowercased first letter properties):

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

    Sources:

    • Apple developer documentation: "Encoding and Decoding Custom Types"
    • WWDC 2017 session 212: "What's new in Foundation"
    • MartianCraft: "Implementing a custom key strategy for coding types"
    0 讨论(0)
  • 2020-11-22 15:54

    Manually customising coding keys

    In your example, you're getting an auto-generated conformance to Codable as all your properties also conform to Codable. This conformance automatically creates a key type that simply corresponds to the property names – which is then used in order to encode to/decode from a single keyed container.

    However one really neat feature of this auto-generated conformance is that if you define a nested enum in your type called "CodingKeys" (or use a typealias with this name) that conforms to the CodingKey protocol – Swift will automatically use this as the key type. This therefore allows you to easily customise the keys that your properties are encoded/decoded with.

    So what this means is you can just say:

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

    The enum case names need to match the property names, and the raw values of these cases need to match the keys that you're encoding to/decoding from (unless specified otherwise, the raw values of a String enumeration will the same as the case names). Therefore, the zip property will now be encoded/decoded using the key "zip_code".

    The exact rules for the auto-generated Encodable/Decodable conformance are detailed by the evolution proposal (emphasis mine):

    In addition to automatic CodingKey requirement synthesis for enums, Encodable & Decodable requirements can be automatically synthesized for certain types as well:

    1. Types conforming to Encodable whose properties are all Encodable get an automatically generated String-backed CodingKey enum mapping properties to case names. Similarly for Decodable types whose properties are all Decodable

    2. Types falling into (1) — and types which manually provide a CodingKey enum (named CodingKeys, directly, or via a typealias) whose cases map 1-to-1 to Encodable/Decodable properties by name — get automatic synthesis of init(from:) and encode(to:) as appropriate, using those properties and keys

    3. Types which fall into neither (1) nor (2) will have to provide a custom key type if needed and provide their own init(from:) and encode(to:), as appropriate

    Example encoding:

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

    Example decoding:

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

    Automatic snake_case JSON keys for camelCase property names

    In Swift 4.1, if you rename your zip property to zipCode, you can take advantage of the key encoding/decoding strategies on JSONEncoder and JSONDecoder in order to automatically convert coding keys between camelCase and snake_case.

    Example encoding:

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

    Example decoding:

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

    One important thing to note about this strategy however is that it won't be able to round-trip some property names with acronyms or initialisms which, according to the Swift API design guidelines, should be uniformly upper or lower case (depending on the position).

    For example, a property named someURL will be encoded with the key some_url, but on decoding, this will be transformed to someUrl.

    To fix this, you'll have to manually specify the coding key for that property to be string that the decoder expects, e.g someUrl in this case (which will still be transformed to some_url by the encoder):

    struct S : Codable {
    
      private enum CodingKeys : String, CodingKey {
        case someURL = "someUrl", someOtherProperty
      }
    
      var someURL: String
      var someOtherProperty: String
    }
    

    (This doesn't strictly answer your specific question, but given the canonical nature of this Q&A, I feel it's worth including)

    Custom automatic JSON key mapping

    In Swift 4.1, you can take advantage of the custom key encoding/decoding strategies on JSONEncoder and JSONDecoder, allowing you to provide a custom function to map coding keys.

    The function you provide takes a [CodingKey], which represents the coding path for the current point in encoding/decoding (in most cases, you'll only need to consider the last element; that is, the current key). The function returns a CodingKey that will replace the last key in this array.

    For example, UpperCamelCase JSON keys for lowerCamelCase property names:

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

    You can now encode with the .convertToUpperCamelCase key strategy:

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

    and decode with the .convertFromUpperCamelCase key strategy:

    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")
    
    0 讨论(0)
提交回复
热议问题