1

I need your 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.

8
  • 1
    Perhaps this question can be helpful. Commented Jun 16, 2022 at 11:50
  • 1
    Check this out. It might help -- stackoverflow.com/questions/67940691/… Commented Jun 16, 2022 at 12:00
  • Can you distinct an ElementA from an ElementB? I mean with "real difference" in what are they properties, etc.? Does the name properties is linked to the kind of elements? Commented Jun 16, 2022 at 12:14
  • @Larme I should use the name property of the zone to distinguish the elements. Commented Jun 16, 2022 at 12:19
  • What if more than one block for each zone in the array? Commented Jun 16, 2022 at 12:31

2 Answers 2

5

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 variables.

struct Root : Decodable {
    var elementsA = [ElementA]()
    var elementsB = [ElementB]()
    var elementsC = [ElementC]()
    var elementsD = [ElementD]()

    enum Zone: String, Decodable { case zoneA, zoneB, zoneC, zoneD }
    
    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 zone = try item.decode(Zone.self, forKey: .name)
            switch zone {
                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)
            }
        }
    }
}

Decoding the stuff is straightforward

do {
    let result = try JSONDecoder().decode(Root.self, from: Data(jsonString.utf8))
    print(result)
} catch {
    print(error)
}
Sign up to request clarification or add additional context in comments.

3 Comments

Hah! Just finished ten mins ago going through this with a colleague and then it's the first thing I saw on SO. I wish custom decoding wasn't so steeped in mystery for so many developers but it's actually fairly straight forward when you get your head around how it works.
You could use an enum enum Zone: String, Decodable { zoneA, zoneB...} for decoding the zone name, this will simplify a little bit the switch, and makes it more robust.
@Fogmeister Yeah, I really love answering these questions on SO; they're almost always a fun diversion. But yeah, we definitely need more resources for folks to learn how to do this generally. (I'm giving a talk at 360iDev on exactly this in Sep, and it'll probably be the subject of some blog posts once I resurrect my blog.)
1

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:

  1. Retain the original structure (array of zones)
  2. Handle repeating zones (if server sends more than just one for each zone)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.