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

71,895

Solution 1

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

Solution 2

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:

Solution 3

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.

Share:
71,895
chrismanderson
Author by

chrismanderson

Political strategist making the switch to computer scientist.

Updated on January 22, 2021

Comments

  • chrismanderson
    chrismanderson over 3 years

    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

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

    I can encode this to 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" 
               }
        }
    }
    

    I can encode this back to an object.

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

    But If I had a json object that was

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

    How would I tell the decoder on Address that zip_code maps to zip? I believe you use the new CodingKey protocol, but I can't figure out how to use this.

  • chrismanderson
    chrismanderson about 7 years
    Just stumbled on to this myself! I wonder, is there a way to only override the one key that I want to change and leave the rest alone? E.g. in the case statement, under the CodingKeys enum; can I just list the one key I'm changing?
  • Martin R
    Martin R about 7 years
    """ is for a multi-line literal :)
  • Hamish
    Hamish about 7 years
    @MartinR Or even just a single line literal without having to escape "s :D
  • Hamish
    Hamish about 7 years
    @chrismanderson I don't believe so, but really I don't think it's too onerous.
  • chrismanderson
    chrismanderson about 7 years
    It's not that bad. It does make it safe, so you ensure that ever key is covered. It will be easier to handle in Xcode 9 for sure with refactoring. It is a duplicate list, which is never a great thing. But as you said, it's not the worst thing in the world.
  • Hamish
    Hamish about 7 years
    @chrismanderson Exactly – especially given that the compiler enforces that the case names are kept in sync with the property names (it will give you an error saying you're not conforming to Codable otherwise)
  • NikGreen
    NikGreen about 7 years
    do you by any chance know if it's possible to use nested keys like address.street?
  • Hamish
    Hamish about 7 years
    @NikGreen Not quite sure I understand – each nesting layer should really be responsible for managing its own keys (be it auto-generated by the compiler, or specified manually), independent of other layers. Could you possibly give an example of what you mean? If you just mean extracting the value for the street key from the Address, I would just do JSONDecoder().decode(Address.self, from: jsonData).street. You probably could use nested types in order to do for example .decode(Address.Street.self, from: jsonData), but I don't immediately see how you could do that without duplicating keys
  • Clay Ellis
    Clay Ellis about 7 years
    @Hamish I think what @NikGreen means is asking is, can you skip to a nested object using nested keys in JSON in order to avoid creating structs for each layer? So that in the nested CodingKeys enum you could specify case address = "person.house.address" (in some Codable) without having to create Person and House structs if the JSON had an Address nested under { "person": { ... "house": { ... "address": { "state":... } } } }.
  • Clay Ellis
    Clay Ellis about 7 years
    Okay, this page makes it pretty clear how to handle that case: developer.apple.com/documentation/foundation/…. See specifically "Encode and Decode Manually" where it talks about additionalInfo.
  • Hamish
    Hamish about 7 years
    @ClayEllis Ah yes, although of course using the nested containers for example directly in the initialiser of Address unnecessarily ties yourself to decoding a JSON object that starts at a specific place in the parent object graph. It would be much nicer to abstract the starting key path up to the decoder itself – here's a rough hackey-ish implementation.
  • EFE
    EFE over 6 years
    i have to parse {"Accept-Encoding" = ""} and i simply cannot name a var or let like Accept-Encoding because of the '-' character. I keep getting compiler error so this answer the solution of my problem. It's so boring that we can name a variable using emoji but we can't use '-' character.
  • Edison
    Edison about 6 years
    I have a snake case value in an API that is percent_change_24h. Using .convertFromSnakeCase I tried percentChange24h but I'm getting nil. My other snake cases without numerals work i.e. market_cap to marketCap so this seems to be an issue with numerals.
  • Hamish
    Hamish about 6 years
    @tymac convertFromSnakeCase internally appears to use the .capitalized property on (NS)String in order to transform each of the non-first components (those separated by underscores) of the key. "24h".capitalized returns "24H", so you want percentChange24H (you can add this as a custom coding key if you want your property to remain being named percentChange24h).
  • Edison
    Edison about 6 years
    @Hamish What's weird is that I already created a custom coding key using case volume24h = "volume_24h" but I was still seeing nil in the output. Any ideas why? I did however have success with changing it to uppercase like you said. Amazing! Where did you find that info about the internal info? Thanks.
  • Hamish
    Hamish about 6 years
    @tymac Custom coding keys are consulted after performing any given key strategies, so you want case volume24h = "volume24H". And the implementation of the convertFromSnakeCase key strategy is available here :)
  • C0D3
    C0D3 over 5 years
    Great code, thank you @Hamish . Do you mind updating the extension JSONDecoder.KeyDecodingStrategy convertFromUpperCamelCase to swift 4.2 ? I'm trying to do something similar and AnyCodingKey() doesn't exist anymore. Also when I try to replaceSubrange on the key, I get an error saying: "cannot use mutating member on immutable value". I'm trying to use a .custom KeyDecodingStrategy to implement an UPPERCASEKeyCodable extension. Your help would be appreciated.
  • C0D3
    C0D3 over 5 years
    On an update, I managed to do something with it. There is some help here: developer.apple.com/documentation/foundation/jsondecoder/… And there is a custom AnyKey struct and that is used there. This code & help is still appreciated
  • Hamish
    Hamish over 5 years
    @c1pherB1t The code in my answer compiles fine in Swift 4.2 for me. AnyCodingKey isn't a part of the standard library (but really it should it be!), I defined it in an earlier code block in my answer.
  • C0D3
    C0D3 over 5 years
    @Hamish thank you for the response. Oh yes, I just realized that! AnyCodingKey is pretty similar to AnyKey in that link by Apple. I was also a bit confused by using codingKeys.last but I think that's the way Apple wants to iterate over the keys in the .custom callback. I'm good now. Thanks again.
  • Hamish
    Hamish over 5 years
    @c1pherB1t No worries! codingKeys.last gives you current coding key, all the previous elements in the array are parent coding keys for the given coding path. For example when decoding {"foo": {"bar": "baz"}}, when the key decoding strategy is applied to "bar", codingKeys will be ["foo", "bar"].
  • bojan
    bojan almost 5 years
    upvoted because of encoder.keyEncodingStrategy = .convertToSnakeCase :) was looking for the way how to not repeat all properties inside CodingKeys enum. Thanks!
  • Daniel
    Daniel over 4 years
    Just a side note: UpperCamelCase is usually called PascalCase
  • John Kaster
    John Kaster about 4 years
    This is the best SO answer I've seen for any Swift/JSON question. Thank you so much for the clear and thorough discussion and clean, working examples.
  • Crysambrosia
    Crysambrosia about 2 years
    Or you could use decodeIfPresent : label = trackContainer.decodeIfPresent(String.self, forKey: .label) ?? ""