I need you help to implement a custom JSON decoding. The JSON returned by the API is:
{
"zones": [
{
"name": "zoneA",
"blocks": [
// an array of objects of type ElementA
]
},
{
"name": "zoneB",
"blocks": [
// an array of objects of type ElementB
]
},
{
"name": "zoneC",
"blocks": [
// an array of objects of type ElementC
]
},
{
"name": "zoneD",
"blocks": [
// an array of objects of type ElementD
]
}
]
}
I don't want to parse this JSON as an array of zones with no meaning. I'd like to produce a model with an array for every specific type of block, like this:
struct Root {
let elementsA: [ElementA]
let elementsB: [ElementB]
let elementsC: [ElementC]
let elementsD: [ElementD]
}
How can I implement the Decodable
protocol (by using init(from decoder:)
) to follow this logic? Thank you.
CodePudding user response:
This is a solution with nested containers. With the given (simplified but valid) JSON string
let jsonString = """
{
"zones": [
{
"name": "zoneA",
"blocks": [
{"name": "Foo"}
]
},
{
"name": "zoneB",
"blocks": [
{"street":"Broadway", "city":"New York"}
]
},
{
"name": "zoneC",
"blocks": [
{"email": "[email protected]"}
]
},
{
"name": "zoneD",
"blocks": [
{"phone": "555-01234"}
]
}
]
}
"""
and the corresponding element structs
struct ElementA : Decodable { let name: String }
struct ElementB : Decodable { let street, city: String }
struct ElementC : Decodable { let email: String }
struct ElementD : Decodable { let phone: String }
first decode the zones
as nestedUnkeyedContainer
then iterate the array and decode first the name
key and depending on name
the elements.
Side note: This way requires to declare the element arrays as var
iables.
struct Root : Decodable {
var elementsA = [ElementA]()
var elementsB = [ElementB]()
var elementsC = [ElementC]()
var elementsD = [ElementD]()
private enum CodingKeys: String, CodingKey { case zones }
private enum ZoneCodingKeys: String, CodingKey { case name, blocks }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var zonesContainer = try container.nestedUnkeyedContainer(forKey: .zones)
while !zonesContainer.isAtEnd {
let item = try zonesContainer.nestedContainer(keyedBy: ZoneCodingKeys.self)
let name = try item.decode(String.self, forKey: .name)
switch name {
case "zoneA": elementsA = try item.decode([ElementA].self, forKey: .blocks)
case "zoneB": elementsB = try item.decode([ElementB].self, forKey: .blocks)
case "zoneC": elementsC = try item.decode([ElementC].self, forKey: .blocks)
case "zoneD": elementsD = try item.decode([ElementD].self, forKey: .blocks)
default: throw DecodingError.dataCorruptedError(forKey: .blocks, in: item, debugDescription: "Name \(name) is not supported")
}
}
}
}
Decoding the stuff is straightforward
do {
let result = try JSONDecoder().decode(Root.self, from: Data(jsonString.utf8))
print(result)
} catch {
print(error)
}
CodePudding user response:
the "zone" property is an array of Zone
objects. so you can decode them like:
enum Zone: Decodable {
case a([ElementA])
case b([ElementB])
case c([ElementC])
case d([ElementD])
enum Name: String, Codable {
case a = "zoneA"
case b = "zoneB"
case c = "zoneC"
case d = "zoneD"
}
enum RootKey: CodingKey {
case name
case blocks
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootKey.self)
let zoneName = try container.decode(Name.self, forKey: .name)
switch zoneName {
case .a: try self = .a(container.decode([ElementA].self, forKey: .blocks))
case .b: try self = .b(container.decode([ElementB].self, forKey: .blocks))
case .c: try self = .c(container.decode([ElementC].self, forKey: .blocks))
case .d: try self = .d(container.decode([ElementD].self, forKey: .blocks))
}
}
}
Then you can filter out anything you like. For example you can pass in the array and get the result you asked in your question:
struct Root {
init(zones: [Zone]) {
elementsA = zones.reduce([]) {
guard case let .a(elements) = $1 else { return $0 }
return $0 elements
}
elementsB = zones.reduce([]) {
guard case let .b(elements) = $1 else { return $0 }
return $0 elements
}
elementsC = zones.reduce([]) {
guard case let .c(elements) = $1 else { return $0 }
return $0 elements
}
elementsD = zones.reduce([]) {
guard case let .d(elements) = $1 else { return $0 }
return $0 elements
}
}
let elementsA: [ElementA]
let elementsB: [ElementB]
let elementsC: [ElementC]
let elementsD: [ElementD]
}
✅ Benefits:
- Retain the original structure (array of zones)
- Handle repeating zones (if server sends more than just one for each zone)