Reputation: 91
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
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
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
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