skip to Main Content

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.

2

Answers


  1. 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)
    }
    
    Login or Signup to reply.
  2. 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)
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search