Reputation: 21
Consider the following JSON:
{
"jsonName": "fluffy",
"color1": "Blue",
"color2": "Red",
"color3": "Green",
"color4": "Yellow",
"color5": "Purple"
}
And the model object:
struct Cat: Decodable {
let name: String
let colors: [String]
private enum CodingKeys: String, CodingKey {
case name = "jsonName"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
// How to parse all the items with colorX into the colors property???
}
}
There can be up to 10 colors, but some of them may be empty strings.
I've tried several variations of the decode
method but I can't figure out how to use an identifier like color\(i)
.
Upvotes: 2
Views: 125
Reputation: 285220
An alternative to Joakim's, Rob's and workingdog's answer is to decode the JSON as [String:String]
dictionary in a singleValueContainer
and map the data accordingly. The solution doesn't need any CodingKey
. localizedStandardCompare
sorts the keys in the proper numeric order if this matters. If it doesn't matter at all omit sorted
. Further the "empty-string" colors are filtered out.
struct Cat: Decodable {
let name: String
let colors: [String]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dictionary = try container.decode([String:String].self)
guard let name = dictionary["jsonName"] else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Key `name` not found") }
self.name = name
colors = dictionary
.keys
.sorted{$0.localizedStandardCompare($1) == .orderedAscending}
.filter{$0.hasPrefix("color")}
.compactMap {
guard let color = dictionary[$0], !color.isEmpty else { return nil }
return color
}
}
}
I know there is a keyNotFound
decoding error but in a singleValueContainer
it's pretty complicated to create one.
Upvotes: 1
Reputation: 36807
try this simple approach using a while
loop inside the init(from decoder: Decoder)
struct Cat: Decodable {
let name: String
var colors: [String] // <-- here var
private enum CodingKeys: String, CodingKey {
case name = "jsonName"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.name = try values.decode(String.self, forKey: .name)
// -- here
self.colors = []
let container = try decoder.singleValueContainer()
let colours = try container.decode([String: String?].self)
var index = 1
while
let col = colours["color\(index)"] as? String,
!col.isEmpty
{
self.colors.append(col)
index += 1
}
// or alternatively, if you have gaps in the color indices, eg no color3
// for i in 1..<11 {
// if let col = colours["color\(i)"] as? String, !col.isEmpty {
// self.colors.append(col)
// }
// }
}
}
Upvotes: 1
Reputation: 273540
Similar to Rob Napier's answer, I would also implement a custom CodingKey
type to decode the colour keys, in addition to a regular enum CodingKey
representing the non-colour keys.
struct Cat: Decodable {
let name: String
let colors: [String]
enum GeneralCodingKeys: String, CodingKey {
case name = "jsonName"
// other non-color coding keys...
}
struct ColorKey: CodingKey {
let number: Int
init(_ number: Int) { self.number = number }
init?(intValue: Int) { nil }
var intValue: Int? { nil }
var stringValue: String { "color\(number)" }
init?(stringValue: String) {
if let (_, match) = stringValue.wholeMatch(of: /color(\d+)/)?.output,
let number = Int(match) {
self = .init(number)
} else {
return nil
}
}
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: GeneralCodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
let colorContainer = try decoder.container(keyedBy: ColorKey.self)
var colors: [String] = []
for key in colorContainer.allKeys {
colors.append(try colorContainer.decode(String.self, forKey: key))
}
self.colors = colors
}
}
In ColorKey
, I used a regex to do a much stricter validation (compared to Rob Napier's answer) on the key. If you don't need that, you can also just check hasPrefix("color")
:
struct ColorKey: CodingKey {
let key: String
init(_ key: String) { self.key = key }
init?(intValue: Int) { nil }
var intValue: Int? { nil }
var stringValue: String { key }
init?(stringValue: String) {
if stringValue.hasPrefix("color") {
self = .init(stringValue)
} else {
return nil
}
}
}
Upvotes: 2
Reputation: 299565
As with so many of these problems, the tool to start with is AnyCodingKey:
public struct AnyCodingKey: CodingKey, CustomStringConvertible, ExpressibleByStringLiteral,
ExpressibleByIntegerLiteral, Hashable, Comparable {
public var description: String { stringValue }
public let stringValue: String
public init(_ string: String) { self.stringValue = string }
public init?(stringValue: String) { self.init(stringValue) }
public var intValue: Int?
public init(intValue: Int) {
self.stringValue = "\(intValue)"
self.intValue = intValue
}
public init(stringLiteral value: String) { self.init(value) }
public init(integerLiteral value: Int) { self.init(intValue: value) }
public static func < (lhs: AnyCodingKey, rhs: AnyCodingKey) -> Bool {
lhs.stringValue < rhs.stringValue
}
}
You can build this more simply, but this implementation is kind of nice.
With that, one possible decoder looks like this:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: AnyCodingKey.self)
name = try container.decode(String.self, forKey: "jsonName")
var colors: [String] = []
for key in container.allKeys where key.stringValue.hasPrefix("color") {
colors.append(try container.decode(String.self, forKey: key))
}
self.colors = colors
}
This decodes "jsonName"
as name
, and then puts anything starting with "color"
into colors
. You can adapt to a wide variety of specific use cases.
Upvotes: 1