Reputation: 2249
Is it possible to use multiple CodingKeys for a single property?
struct Foo: Decodable {
enum CodingKeys: String, CodingKey {
case contentIDs = "contentIds" || "Ids" || "IDs" // something like this?
}
let contentIDs: [UUID]
}
Upvotes: 16
Views: 9056
Reputation: 61
A better and more cleaner solution could be to create an extension on KeyedDecodingContainer which can be reused.
extension KeyedDecodingContainer{
enum ParsingError:Error{
case noKeyFound
/*
Add other errors here for more use cases
*/
}
func decode<T>(_ type:T.Type, forKeys keys:[K]) throws -> T where T:Decodable {
for key in keys{
if let val = try? self.decode(type, forKey: key){
return val
}
}
throw ParsingError.noKeyFound
}
}
Above function can be used as follows:
struct Foo:Decodable{
let name:String
let contentIDs: [String]
enum CodingKeys:String,CodingKey {
case name
case contentIds, Ids, IDs
}
init(from decoder:Decoder) throws{
let container = try decoder.container(keyedBy:CodingKeys.self)
contentIDs = try container.decode([String].self, forKeys:[.contentIds,.IDs,.Ids])
name = try container.decode(String.self, forKey: .name)
}
}
Upvotes: 6
Reputation: 196
You can do that by using multiple CodingKey enums and custom initializer. Let me show it by an example
enum CodingKeys: String, CodingKey {
case name = "prettyName"
}
enum AnotherCodingKeys: String, CodingKey {
case name
}
init(from decoder: Decoder) throws {
let condition = true // or false
if condition {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
} else {
let values = try decoder.container(keyedBy: AnotherCodingKeys.self)
name = try values.decode(String.self, forKey: .name)
}
}
Upvotes: 13
Reputation: 299275
You can't do literally what you're describing, but you can make the process quite mechanical, and you can turn this into autogenerated code using Sourcery if you need that.
First, as usual, you need an AnyKey
(someday I hope this is added to stdlib; even the Apple docs reference it....)
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
Then you want a new method that can decode from a list of possible keys. This particular implementation tries elements in the dictionary, and then falls back to the name of the key. (This way you don't have to put thing in the dictionary if they only have their own name.)
extension KeyedDecodingContainer where K == AnyKey {
func decode<T>(_ type: T.Type, forMappedKey key: String, in keyMap: [String: [String]]) throws -> T where T : Decodable{
for key in keyMap[key] ?? [] {
if let value = try? decode(T.self, forKey: AnyKey(stringValue: key)) {
return value
}
}
return try decode(T.self, forKey: AnyKey(stringValue: key))
}
}
And finally, the tedious but simple (and code-generated if you like) init:
init(from decoder: Decoder) throws {
let keyMap = [
"contentIDs": ["contentIds", "Ids", "IDs"],
"title": ["name"],
]
let container = try decoder.container(keyedBy: AnyKey.self)
self.contentIDs = try container.decode([UUID].self, forMappedKey: "contentIDs", in: keyMap)
self.title = try container.decode(String.self, forMappedKey: "title", in: keyMap)
self.count = try container.decode(Int.self, forMappedKey: "count", in: keyMap)
}
You can make it even tidier with a local function:
init(from decoder: Decoder) throws {
let keyMap = [
"contentIDs": ["contentIds", "Ids", "IDs"],
"title": ["name"],
]
let container = try decoder.container(keyedBy: AnyKey.self)
func decode<Value>(_ key: String) throws -> Value where Value: Decodable {
return try container.decode(Value.self, forMappedKey: key, in: keyMap)
}
self.contentIDs = try decode("contentIDs")
self.title = try decode("title")
self.count = try decode("count")
// ...
}
I don't think you can get much simpler than this using Decodable, however, because you can't decode dynamic types and Swift needs to be certain you initialize all the properties. (These make it very hard to create a for
loop to do initialization.)
Upvotes: 5
Reputation: 534925
You can't do what you're asking to do. From the question and your later comments, it appears you've got some very bad JSON. Decodable is not made for that sort of thing. Use JSONSerialization and clean up the mess afterward.
Upvotes: 0
Reputation: 24341
Implement the init(from:)
initialiser and add custom parsing as per your requirement, i.e.
struct Foo: Decodable {
let contentIDs: [String]
enum CodingKeys: String, CodingKey, CaseIterable {
case contentIds, Ids, IDs
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let key = container.allKeys.filter({ CodingKeys.allCases.contains($0) }).first, let ids = try container.decodeIfPresent([String].self, forKey: key) {
self.contentIDs = ids
} else {
self.contentIDs = []
}
}
}
Upvotes: 6