2

I have the following JSON object which is to be converted to an object using JSONDecoder:

{
  "first_key": 3,
  "image_1000x1000": "location"
}

This maps to the following Swift model:

struct Response: Decodable {
  var firstKey: Int
  var image1000x1000: String
}

By using JSONDecoder with the .convertFromSnakeCase option, the snake_case keys inside the JSON are transformed to camelCase by using the algorithm defined in the documentation:

This strategy follows these steps to convert JSON keys to camel-case:

  1. Capitalize each word that follows an underscore.

  2. Remove all underscores that aren't at the very start or end of the string.

  3. Combine the words into a single string.

Therefore, in this case:

  • first_key becomes firstKey (as expected)
  • image_1000x1000 should become image1000x1000

However when attempting to decode this response, a keyNotFound error for the image1000x1000 key is thrown (see this live example):

let json = "{\"first_key\": 3, \"image_1000x1000\": \"location\"}".data(using: .utf8)!
do {
  let decoder = JSONDecoder()
  decoder.keyDecodingStrategy = .convertFromSnakeCase
  let response = try decoder.decode(Response.self, from: json)
  print(response)
} catch let e {
  print(e)
}

What is incorrect about my camel case conversion of image_1000x1000, and why can’t JSONDecoder find the corresponding key?

Jack Greenhill
  • 10,240
  • 12
  • 38
  • 70
  • Your struct property is named `image`, not `image1000x1000`. – rmaddy Jun 27 '19 at 01:15
  • @rmaddy That's why I'm using `CodingKeys` to map the JSON key to the corresponding `image` property – Jack Greenhill Jun 27 '19 at 01:22
  • I could be mistaken but if you explicitly list a mapping in the CodingKeys enum, the keyDecodingStrategy isn't applied for the pair. With the key decoding strategy you have in place, it would work if you removed the explicit CodingKeys entry for image/image_1000x1000 and your property was named `image1000x1000`. If you want the property named `image` then you need the CodingKeys entry as you had it originally so it matches the actual JSON key. – rmaddy Jun 27 '19 at 01:29
  • Yeah, the issue persists regardless of whether `CodingKeys` is used (see edit) – Jack Greenhill Jun 27 '19 at 01:36
  • Simple. Just don't use convertFromSnakeCase and use `struct Response: Decodable { let firstKey: Int let image1000x1000: String private enum CodingKeys: String, CodingKey { case firstKey = "first_key", image1000x1000 = "image_1000x1000" } }` – Leo Dabus Jun 27 '19 at 02:08
  • @LeoDabus Not really solving the issue, is it? I would ideally like to know why this doesn't work. – Jack Greenhill Jun 27 '19 at 02:14
  • the code above works if you don't set to convert the case. Your attempt doesn't work because a number has no case. You have to manually set the custom coding keys. AFAIK there is no way to work it out automatically – Leo Dabus Jun 27 '19 at 02:17
  • Sorry, misinterpreted what you suggested. My first attempt involved specifying `CodingKeys` explicitly, but it doesn't work in that case either. – Jack Greenhill Jun 27 '19 at 02:29
  • https://repl.it/@LeonardoSavio/InternalSmugPrinters this prints `Response(firstKey: 3, image1000x1000: "location")` – Leo Dabus Jun 27 '19 at 02:35
  • Still fails when `.convertFromSnakeCase` is used. I suppose using `CodingKeys` to map keys from their snake_case to camelCase equivalent _would work_, but for my real-world use case that would involve a lot of verbose repetition – my thinking is that `.convertFromSnakeCase` should still be able to handle all of that for me. – Jack Greenhill Jun 27 '19 at 02:47

2 Answers2

2

You can see what the algorithm is expecting by running the process in reverse; use a JSONEncoder to encoder your data and inspect the output:

struct Response: Codable {
    var firstKey: Int
    var image1000x1000: String
}

let test = Response(firstKey: 10, image1000x1000: "secondKey" )

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(test)
print(String(data: data, encoding: .utf8)!)

This will produce:

{"first_key":10,"image1000x1000":"secondKey"}

So if you have control over the JSON and can live with image1000x1000 as a key, then you're done. If not you'll have to do something like this:

struct Response: Codable {
    var firstKey: Int
    var image1000x1000: String

    private enum CodingKeys: String, CodingKey {
        case image1000x1000 = "image_1000x1000"
        case firstKey = "first_key"
    }
}

Another option is to implement a custom key encoding strategy. It may end up being less code. See KeyEncodingStrategy for more about this.

idz
  • 12,825
  • 1
  • 29
  • 40
0

I am facing the same issue my json keys are iso639_1 and iso639_2. Without adding Coding key it will works.

make your variable optional

Here it's my Decodable model

struct CountryModel: Decodable{
    var name: String
    var borders: [String]
    var region: String
    var alpha2Code: String
    var alpha3Code: String
    var flag: String
    var languages: [languages]
}

struct languages: Decodable {
    var iso639_1: String?
    var iso639_2: String?
    var name: String
    var nativeName: String
}

When I add optional to both underscore variables iso639_1 and iso639_2. Then it works fine may be due to Null Value!

Here, in your case add optional to your image1000x1000 variable. Like below

struct Response: Decodable {
  var firstKey: Int
  var image_1000x1000: String?
}

Hope it will works!

Yogesh Patel
  • 1,893
  • 1
  • 20
  • 55