Home > Enterprise >  Custom JSON decoding: decode an array of different objects in several distinct arrays
Custom JSON decoding: decode an array of different objects in several distinct arrays

Time:06-16

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

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:

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