Swift 4 Decodable with keys not known until decoding time

JsonSwift4Decodable

Json Problem Overview


How does the Swift 4 Decodable protocol cope with a dictionary containing a key whose name is not known until runtime? For example:

  [
    {
      "categoryName": "Trending",
      "Trending": [
        {
          "category": "Trending",
          "trailerPrice": "",
          "isFavourit": null,
          "isWatchlist": null
        }
      ]
    },
    {
      "categoryName": "Comedy",
      "Comedy": [
        {
          "category": "Comedy",
          "trailerPrice": "",
          "isFavourit": null,
          "isWatchlist": null
        }
      ]
    }
  ]

Here we have an array of dictionaries; the first has keys categoryName and Trending, while the second has keys categoryName and Comedy. The value of the categoryName key tells me the name of the second key. How do I express that using Decodable?

Json Solutions


Solution 1 - Json

The key is in how you define the CodingKeys property. While it's most commonly an enum it can be anything that conforms to the CodingKey protocol. And to make dynamic keys, you can call a static function:

struct Category: Decodable {
	struct Detail: Decodable {
		var category: String
		var trailerPrice: String
		var isFavorite: Bool?
		var isWatchlist: Bool?
	}
	
	var name: String
	var detail: Detail
	
	private struct CodingKeys: CodingKey {
		var intValue: Int?
		var stringValue: String
		
		init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
		init?(stringValue: String) { self.stringValue = stringValue }
		
		static let name = CodingKeys.make(key: "categoryName")
		static func make(key: String) -> CodingKeys {
			return CodingKeys(stringValue: key)!
		}
	}
	
	init(from coder: Decoder) throws {
		let container = try coder.container(keyedBy: CodingKeys.self)
		self.name = try container.decode(String.self, forKey: .name)
		self.detail = try container.decode([Detail].self, forKey: .make(key: name)).first!
	}
}

Usage:

let jsonData = """
  [
    {
      "categoryName": "Trending",
      "Trending": [
        {
          "category": "Trending",
          "trailerPrice": "",
          "isFavourite": null,
          "isWatchlist": null
        }
      ]
    },
    {
      "categoryName": "Comedy",
      "Comedy": [
        {
          "category": "Comedy",
          "trailerPrice": "",
          "isFavourite": null,
          "isWatchlist": null
        }
      ]
    }
  ]
""".data(using: .utf8)!

let categories = try! JSONDecoder().decode([Category].self, from: jsonData)

(I changed isFavourit in the JSON to isFavourite since I thought it was a mispelling. It's easy enough to adapt the code if that's not the case)

Solution 2 - Json

You can write a custom struct that functions as a CodingKeys object, and initialize it with a string such that it extracts the key you specified:

private struct CK : CodingKey {
    var stringValue: String
    init?(stringValue: String) {
        self.stringValue = stringValue
    }
    var intValue: Int?
    init?(intValue: Int) {
        return nil
    }
}

Thus, once you know what the desired key is, you can say (in the init(from:) override:

let key = // whatever the key name turns out to be
let con2 = try! decoder.container(keyedBy: CK.self)
self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)

So what I ended up doing is making two containers from the decoder — one using the standard CodingKeys enum to extract the value of the "categoryName" key, and another using the CK struct to extract the value of the key whose name we just learned:

init(from decoder: Decoder) throws {
    let con = try! decoder.container(keyedBy: CodingKeys.self)
    self.categoryName = try! con.decode(String.self, forKey:.categoryName)
    let key = self.categoryName
    let con2 = try! decoder.container(keyedBy: CK.self)
    self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)
}

Here, then, is my entire Decodable struct:

struct ResponseData : Codable {
    let categoryName : String
    let unknown : [Inner]
    struct Inner : Codable {
        let category : String
        let trailerPrice : String
        let isFavourit : String?
        let isWatchList : String?
    }
    private enum CodingKeys : String, CodingKey {
        case categoryName
    }
    private struct CK : CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }
        var intValue: Int?
        init?(intValue: Int) {
            return nil
        }
    }
    init(from decoder: Decoder) throws {
        let con = try! decoder.container(keyedBy: CodingKeys.self)
        self.categoryName = try! con.decode(String.self, forKey:.categoryName)
        let key = self.categoryName
        let con2 = try! decoder.container(keyedBy: CK.self)
        self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)
    }
}

And here's the test bed:

    let json = """
      [
        {
          "categoryName": "Trending",
          "Trending": [
            {
              "category": "Trending",
              "trailerPrice": "",
              "isFavourit": null,
              "isWatchlist": null
            }
          ]
        },
        {
          "categoryName": "Comedy",
          "Comedy": [
            {
              "category": "Comedy",
              "trailerPrice": "",
              "isFavourit": null,
              "isWatchlist": null
            }
          ]
        }
      ]
    """
    let myjson = try! JSONDecoder().decode(
        [ResponseData].self, 
        from: json.data(using: .utf8)!)
    print(myjson)

And here's the output of the print statement, proving that we've populated our structs correctly:

[JustPlaying.ResponseData(
    categoryName: "Trending", 
    unknown: [JustPlaying.ResponseData.Inner(
        category: "Trending", 
        trailerPrice: "", 
        isFavourit: nil, 
        isWatchList: nil)]), 
 JustPlaying.ResponseData(
    categoryName: "Comedy", 
    unknown: [JustPlaying.ResponseData.Inner(
        category: "Comedy", 
        trailerPrice: "", 
        isFavourit: nil, 
        isWatchList: nil)])
]

Of course in real life we'd have some error-handling, no doubt!


EDIT Later I realized (in part thanks to CodeDifferent's answer) that I didn't need two containers; I can eliminate the CodingKeys enum, and my CK struct can do all the work! It is a general purpose key-maker:

init(from decoder: Decoder) throws {
    let con = try! decoder.container(keyedBy: CK.self)
    self.categoryName = try! con.decode(String.self, forKey:CK(stringValue:"categoryName")!)
    let key = self.categoryName
    self.unknown = try! con.decode([Inner].self, forKey: CK(stringValue:key)!)
}

Solution 3 - Json

Here's what I eventually came up for this json:

let json = """
{
    "BTC_BCN":{
        "last":"0.00000057",
        "percentChange":"0.03636363",
        "baseVolume":"47.08463318"
    },
    "BTC_BELA":{
        "last":"0.00001281",
        "percentChange":"0.07376362",
        "baseVolume":"5.46595029"
    }
}
""".data(using: .utf8)!

We make such a structure:

struct Pair {
    let name: String
    let details: Details

    struct Details: Codable {
        let last, percentChange, baseVolume: String
    }
}

then decode:

if let pairsDictionary = try? JSONDecoder().decode([String: Pair.Details].self, from: json) {
    
    var pairs: [Pair] = []
    for (name, details) in pairsDictionary {
        let pair = Pair(name: name, details: details)
        pairs.append(pair)
    }
    
    print(pairs)
}

It is also possible to call not pair.details.baseVolume, but pair.baseVolume:

struct Pair {
    ......
    var baseVolume: String { return details.baseVolume }
    ......

Or write custom init:

struct Pair {
    .....
    let baseVolume: String
    init(name: String, details: Details) {
         self.baseVolume = details.baseVolume
    ......

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionmattView Question on Stackoverflow
Solution 1 - JsonCode DifferentView Answer on Stackoverflow
Solution 2 - JsonmattView Answer on Stackoverflow
Solution 3 - JsonvbbView Answer on Stackoverflow