2

I am calling an API and then decoding it with the simplified code below


guard let url = URL(string: "someURL") else {
    return
}

let task = URLSession.shared.dataTask(with: url) { data, response, error in

    let decoder = JSONDecoder()
    if let data = data {
        do {
            let results = try decoder.decode(Response.self, from: data)
            print(results)
        } catch {
            print(error)
        }
    }
}
task.resume()

Where Response is my struct seen below

struct Response : Codable {
    let response: ResponseContents
}

struct ResponseContents : Codable {
    let result : [wantedData]
}

struct wantedData : Codable {
    let name: String
}

For the most part this works well however, sometimes the API returns a JSON that does not have a key called name and instead the key is otherName - therefore I get an error saying 'keyNotFound'.

Is there a way I can add a conditional statement in my struct or parsing statement that checks to see if the key is not found and if not it uses another one that I define?

James Taga
  • 65
  • 4
  • You could have 2 optional properties: `let name: String?; let otherName: String?`, and when you want to access the property, you could use a computed property: `var nameToUse: String { name ?? otherName }`? – Larme Jul 26 '22 at 16:06

2 Answers2

1

The solution given from Larme is good as well.

But you also can use Decodable with CodingKey directly, harder to read, but reusable.

struct DynamicKey: CodingKey {
    var stringValue: String
    init?(stringValue: String) {
        self.stringValue = stringValue
    }
}


struct wantedData : Codable {
    let name: String

 init(from decoder: Decoder) throws {
      let dynamicKeysContainer = try decoder.container(keyedBy: DynamicKey.self)
      try dynamicKeysContainer.allKeys.forEach { key in
          switch key.stringValue {
          case "name" where try dynamicKeysContainer.decode(String.self, forKey: key):
              name = $0
          case "otherName" where try dynamicKeysContainer.decode(String.self, forKey: key):
              name = $0
          default: break
          }
      }
  }
} 

I didn't try it. And you can probably do better, just post that to help.

Maurice
  • 2,129
  • 2
  • 25
  • 33
1

You just need an extra decoding key, and a manual decoder:

For Data like this, where "name" and "otherName" are possible parameters:

let json = Data("""
[
    {
        "name": "Alice",
        "age": 43
    },
    {
        "otherName": "Bob",
        "age": 25
    }
]
""".utf8)

And a struct like this:

struct Person {
    var name: String
    var age: Int
}

Then decoding looks like this:

extension Person: Decodable {
    enum CodingKeys: CodingKey {
        case name
        case otherName  // Keys do not have to be the same as properties
        case age
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        // Try both approaches:
        self.name = try container.decodeIfPresent(String.self, forKey: .name) ??
                        container.decode(String.self, forKey: .otherName)

        // Other, default handling
        self.age = try container.decode(Int.self, forKey: .age)
    }
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610