After trying practically everything, I found that the best approach to decode the JSON is through the UnkeyedDecodingContainer
protocol.
According to the documentation, an unkeyed container "is used to hold the encoded properties of a decodable type sequentially, without keys." This describes a perfect match for the given JSON structure.
Since Codable
is just an alias for Decodable
and Encodable
, let's make Items
conform to Decodable
first before Encodable
.
Decodable
Conformance
Given this Codable
structure that holds bottom-level JSON objects:
struct NameVersion: Codable {
let name: String?
let version: String?
}
To decode the JSON:
indirect enum Items: Codable {
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = []
var unkeyedValues = try decoder.unkeyedContainer()
while unkeyedValues.count! > unkeyedValues.currentIndex {
let containerIndexBeforeLoop = unkeyedValues.currentIndex
if let nameVersion = try? unkeyedValues.decode(NameVersion.self) {
itemsSet.insert(Items.item(nameVersion))
}
else if let anyOfItems = try? unkeyedValues.decode(AnyOfItems.self) {
itemsSet.insert(anyOfItems.items)
}
else if let items = try? unkeyedValues.decode(Items.self) {
itemsSet.insert(items)
}
if unkeyedValues.currentIndex <= containerIndexBeforeLoop { break }
}
if itemsSet.count == 1 {
self = ItemsSet.popFirst()!
} else {
self = .allOfItems(itemsSet)
}
}
func encode(to encoder: Encoder) throws {
}
case item(NameVersion)
case anyOfItems(Set<Items>)
case allOfItems(Set<Items>)
}
Although there exists a .nestedContainer()
method for getting a nested keyed container from within the unkeyed container, that holds the data of an { "any_of": [] }
JSON object, the nested container is unable to call the decode(forKey:, from:)
method to decode JSON.
Instead, I followed this solution for decoding nested data, and created the following service struct for decoding { "any_of": [] }
JSON objects.
struct AnyOfItems: Codable {
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = []
var unkeyedValues = try decoder.unkeyedContainer()
while unkeyedValues.count! > unkeyedValues.currentIndex {
let containerIndexBeforeLoop = unkeyedValues.currentIndex
if let nameVersion = try? unkeyedValues.decode(NameVersion.self) {
itemsSet.insert(Items.item(nameVersion))
} else if let anyOfItems = try? unkeyedValues.decode(AnyOfItems.self) {
itemsSet.insert(anyOfItems.items)
} else if let items = try? unkeyedValues.decode(Items.self) {
itemsSet.insert(items)
}
if unkeyedValues.currentIndex <= containerIndexBeforeLoop { break }
}
if itemsSet.count == 1 {
items = itemsSet.popFirst()!
} else {
itsms = Items.anyOfItems(itemsSet)
}
}
let items: Items
}
The majority of the repeated code can be extracted to its own function:
indirect enum Items: Codable {
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = try decodeItems(from: decoder)
if itemsSet.count == 1 { self = ItemsSet.popFirst()! }
else { self = .allOfItems(itemsSet) }
}
func encode(to encoder: Encoder) throws {
}
case item(NameVersion)
case anyOfItems(Set<Items>)
case allOfItems(Set<Items>)
}
struct AnyOfItems: Codable {
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = try decodeItems(from: decoder)
if itemsSet.count == 1 { items = itemsSet.popFirst()! }
else { items = Items.anyOfItems(itemsSet) }
}
let items: Items
}
func decodeItems(from decoder: Decoder) throws -> Set<Items> {
var itemsSet: Set<Items> = []
var unkeyedValues = try decoder.unkeyedContainer()
while unkeyedValues.count! > unkeyedValues.currentIndex {
let containerIndexBeforeLoop = unkeyedValues.currentIndex
if let nameVersion = try? unkeyedValues.decode(NameVersion.self) {
itemsSet.insert(Items.item(nameVersion))
} else if let anyOfItems = try? unkeyedValues.decode(AnyOfItems.self) {
itemsSet.insert(anyOfItems.items)
} else if let items = try? unkeyedValues.decode(Items.self) {
itemsSet.insert(items)
}
if unkeyedValues.currentIndex <= containerIndexBeforeLoop { break }
}
return itemsSet
}
Encodable
Conformance
Encoding is much simpler.
indirect enum Items: Codable {
init(from decoder: Decoder) throws {
}
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
switch self {
case .item(let item):
try container.encode(item)
case .allOfItems(let items):
try container.encode(contentsOf: items)
case .anyOfItems(let items):
try container.encode(AnyOfItems(Items.anyOfItems(items)))
}
}
case item(NameVersion)
case anyOfItems(Set<Items>)
case allOfItems(Set<Items>)
}
struct AnyOfItems: Codable {
init(from decoder: Decoder) throws {
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(items, forKey: .items)
}
init(_ items: Items) {
self.items = items
}
let items: Items
private enum CodingKeys: String, CodingKey {
case items = "any_of"
}
}
Codable
Conformance
Finally, with everything put together:
indirect enum Items: Codable {
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = try decodeItems(from: decoder)
if itemsSet.count == 1 {
self = ItemsSet.popFirst()!
} else {
self = .allOfItems(itemsSet)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
switch self {
case .item(let item):
try container.encode(item)
case .allOfItems(let items):
try container.encode(contentsOf: items)
case .anyOfItems(let items):
try container.encode(AnyOfItems(Items.anyOfItems(items)))
}
}
case item(NameVersion)
case anyOfItems(Set<Items>)
case allOfItems(Set<Items>)
}
struct AnyOfItems: Codable {
init(from decoder: Decoder) throws {
var itemsSet: Set<Items> = try decodeItems(from: decoder)
if itemsSet.count == 1 {
items = itemsSet.popFirst()!
} else {
items = Items.anyOfItems(itemsSet)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(items, forKey: .items)
}
init(_ items: Items) {
self.items = items
}
let items: Items
private enum CodingKeys: String, CodingKey {
case items = "any_of"
}
}
func decodeItems(from decoder: Decoder) throws -> Set<Items> {
var itemsSet: Set<Items> = []
var unkeyedValues = try decoder.unkeyedContainer()
while unkeyedValues.count! > unkeyedValues.currentIndex {
let containerIndexBeforeLoop = unkeyedValues.currentIndex
if let nameVersion = try? unkeyedValues.decode(NameVersion.self) {
itemsSet.insert(Items.item(nameVersion))
} else if let anyOfItems = try? unkeyedValues.decode(AnyOfItems.self) {
itemsSet.insert(anyOfItems.items)
} else if let items = try? unkeyedValues.decode(Items.self) {
itemsSet.insert(items)
}
if unkeyedValues.currentIndex <= containerIndexBeforeLoop { break }
}
return itemsSet
}
Codable
protocol. I think I found a solution usingUnkeyedDecodingContainer
, and I'm testing it right now. - Wowbagger and his liquid lunch