1

I'm having trouble to decode a simple JSON api result using structs.

This is the JSON stucture (part of it):

{
  "ad": "Andorra",
  "ae": "United Arab Emirates",
  "af": "Afghanistan",
  "ag": "Antigua and Barbuda",
  "ai": "Anguilla",
  "al": "Albania"
}

This are the structs I created:

struct Countries: Codable {
    var countries: [Country]
}

struct Country: Codable, Identifiable {
    var id = UUID()
    var code: String
    var name: String
}

And using an API I am doing this to try to decode it:

let decodedResponse = try JSONDecoder().decode(Countries.self, from: data)

At the moment this is the error:

No value associated with key CodingKeys(stringValue: "countries", intValue: nil) ("countries").

As I understand correctly the JSON result has two things, the keys and the values. The keys in this case are the country codes (two letters) and the values are the country names. I do want to use both of them in my app but I struggle to use both the key and value using a struct. The error at the moment is also because the dictionary itself has no key. But I can also imagine the values in a single Country will also not work.

2
  • 2
    I would instead decode it into a [String:String] and then manually transform the data into your desired data structure manually by looping over the dictionary. Commented Apr 12, 2021 at 9:51
  • @luk2302 I've tried it like this: let decodedResponse = try JSONDecoder().decode([String: String].self, from: data) DispatchQueue.main.async() { for item in decodedResponse { countries.append(Country(code: item.key, name: item.value)) } } And print(countries) results in:Countries.Country(code: "ar", name: "Argentina"), Countries.Country(code: "gu", name: "Guam"), etc But its not showing any in my list:ForEach(countries, id:\.self) { country in Text(country.name) } Commented Apr 12, 2021 at 10:59

1 Answer 1

3

Implementing custom encoder & decoder logic can help transform between expected json [String:String] and Countries/[Country].

struct Countries: Codable {
  struct Country {
    let code: String
    let name: String
  }
  
  let countries: [Country]

  //custom decoder logic; from json to model
  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    
    let dict = try container.decode([String:String].self)
    countries = dict.map(Country.init)
  }

  //custom encoder logic; from model to json
  func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()

    var dict = [String:String]()
    countries.forEach { (country) in
      dict[country.code] = country.name
    }
    try container.encode(dict)
  }
}

Usage Example**:

let jsonData = """
{
  "ad": "Andorra",
  "ae": "United Arab Emirates",
  "af": "Afghanistan",
  "ag": "Antigua and Barbuda",
  "ai": "Anguilla",
  "al": "Albania"
}
""".data(using: .utf8)!

do {
  let result = try JSONDecoder().decode(Countries.self, from: jsonData)
  result.countries.forEach { print($0) }
  
  let data = try JSONEncoder().encode(result)
  let json = String(data: data, encoding: .utf8)!
  print(json)
} catch {
  print(error)
}

**ignore the force-unwraps, it's just test code

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks! I did manage it to get it working. I've used the forEach to add each country item to the array like this: result.countries.forEach { countries.append($0) } And only used Decodable as I do not need to Encode it back to json.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.