skip to Main Content

Until recently, I have been able to decode both lower case ("fooBar") JSON and pascal case ("FooBar") JSON using the Decodable protocol by simply including a CodingKeys enum like this…

enum CodingKeys: String, CodingKey {
    case bars = "Bars"
}

That has allowed me to decode JSON in either of these forms: {"bars":[]} or {"Bars":[]}

But that no longer works for me.

The complete example is below. When I run this code, only the JSON that has the Pascal case fieldnames are decoded. The only way to decode the lower case JSON is to either change the CodingKeys to lower case (which simply matches the defined fieldnames) or remove them completely.

Example:

import UIKit

struct Foo: Codable {
    var bars: [Bar]
    
    enum CodingKeys: String, CodingKey {
        case bars = "Bars"
    }

    struct Bar: Codable {
        var barBaz: String
        
        enum CodingKeys: String, CodingKey {
            case barBaz = "BarBaz"
        }
    }
}

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let fooBars = [
            "{"bars": [{"barBaz": "abc"}]}",
            "{"Bars": [{"BarBaz": "abc"}]}",
            "{"Bars": [{"BarBaz": "abc"},{"BarBaz": "def"}]}",
            "{"Bars": []}",
            "{"bars": []}"
        ]
        
        fooBars.forEach {
            if let fooBar = $0.data(using: .utf8) {
                do {
                    let data = try JSONDecoder().decode(Foo.self, from: fooBar)
                    print("Success:nFound (data.bars.count) bar(s).n")
                } catch {
                    print("Fail:n(error)n")
                }
            }
        }
    }
    
}

Output:

Fail:
keyNotFound(CodingKeys(stringValue: "Bars", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "Bars", intValue: nil) ("Bars").", underlyingError: nil))

Success:
Found 1 bar(s).

Success:
Found 2 bar(s).

Success:
Found 0 bar(s).

Fail:
keyNotFound(CodingKeys(stringValue: "Bars", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: "Bars", intValue: nil) ("Bars").", underlyingError: nil))

My apps that still reference old Codable classes with this CodingKey approach do still work. So, I suspect I must be doing something wrong in this particular example.

Can anybody please explain what I am doing wrong?

2

Answers


  1. Chosen as BEST ANSWER

    I was able to accomplish this by implementing a custom initializer like this...

    enum CodingKeys: String, CodingKey {
        case bars, Bars
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
                
        if let bars = try? container.decode([Bar].self, forKey: .bars) {
            self.bars = bars
        } else if let bars = try? container.decode([Bar].self, forKey: .Bars) {
            self.bars = bars
        }
    }
    

    This allows me to capture the values as they are being decoded and parse them to their appropriate properties. Now I can read {"bars":[]} and "{Bars:[]}" into the bars property.


  2. Expanding on Joel own answer. Your code will fail silently in case it doesn’t decode your json. Don’t ignore the errors using try?. You should always catch them. Btw data(using: .utf8) will never fail. You can safely force unwrap the result or use Data non falible initializer Data($0.utf8)

    A proper implementation of your custom decoder should be something like this:

    struct Foo: Decodable {
        let bars: [Bar]
        enum CodingKeys: String, CodingKey {
            case bars, Bars
        }
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            do {
                bars = try container.decode([Bar].self, forKey: .bars)
            } catch {
                bars = try container.decode([Bar].self, forKey: .Bars)
            }
        }
        struct Bar: Decodable {
            let barBaz: String
            enum CodingKeys: String, CodingKey {
                case barBaz, BarBaz
            }
            init(from decoder: Decoder) throws {
                let container = try decoder.container(keyedBy: CodingKeys.self)
                do {
                    barBaz = try container.decode(String.self, forKey: .barBaz)
                } catch {
                    barBaz = try container.decode(String.self, forKey: .BarBaz)
                }
            }
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search