Joel S.
Joel S.

Reputation: 91

In Swift, how do I decode both lower case and pascal case JSON using Decodable protocol?

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?

Upvotes: 1

Views: 394

Answers (3)

vadian
vadian

Reputation: 285082

This is a different approach with a custom key decoding strategy, it makes the first character of all CodingKeys lowercase

extension JSONDecoder.KeyDecodingStrategy {
    static var lowerCamelCase: JSONDecoder.KeyDecodingStrategy {
        .custom {
            let currentKey = $0.last!.stringValue
            return AnyKey(stringValue: currentKey.first!.lowercased() + currentKey.dropFirst())! }
    }
}

It requires a custom CodingKey

public struct AnyKey: CodingKey {
   public let stringValue: String
   public init?(stringValue: String) { self.stringValue = stringValue }
   public var intValue: Int? { return nil }
   public init?(intValue: Int) { return nil }
}

Now apply the strategy, it gets rid of the CodingKeys enum

struct Foo: Codable {
    let bars: [Bar]

    struct Bar: Codable {
        let barBaz: String
    }
}

let fooBars = [
    "{\"bars\": [{\"barBaz\": \"abc\"}]}",
    "{\"Bars\": [{\"BarBaz\": \"abc\"}]}",
    "{\"Bars\": [{\"BarBaz\": \"abc\"},{\"BarBaz\": \"def\"}]}",
    "{\"Bars\": []}",
    "{\"bars\": []}"
]

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .lowerCamelCase
fooBars.forEach {
    let data = Data($0.utf8)
    do {
        let fooBar = try decoder.decode(Foo.self, from: data)
        print("Success:\nFound \(fooBar.bars.count) bar(s).\n")
    } catch {
        print("Fail:\n\(error)\n")
    }
}

Upvotes: 0

Leo Dabus
Leo Dabus

Reputation: 236380

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)
            }
        }
    }
}

Upvotes: 1

Joel S.
Joel S.

Reputation: 91

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.

Upvotes: 2

Related Questions