0

I am trying to parse an array of heterogeneous objects using Codable. These objects are also unkeyed as well. I should note that I have the container structure correct, because it DOES loop through and print "it is type1" at all correct times as seen below. I just can't figure out how to access the actual object. Here is my code:

var data: [Any]

public init(from decoder: Decoder) throws {
    var container = try! decoder.container(keyedBy: CodingKeys.self).nestedUnkeyedContainer(forKey: .data)

    while !container.isAtEnd {
        var itemContainer = try container.nestedContainer(keyedBy: CodingKeys.self)

        let itemType = try itemContainer.decode(String.self, forKey: .type)
        switch itemType {
        case "type1":
            print("it is type1")
            // this does not compile, but is what I need
            //let objectOfItem1 = try itemContainer.decode(Type1.self)

            // this compiles, but doesn't work because there is no key with these objects
            //let objectOfItem1 = try itemContainer.decode(Type1, forKey: .type)
        default:
            print("test:: it is the default")
        }
    }
}

private enum CodingKeys: String, CodingKey {
    case data
    case type
}

And here is the JSON I am trying to decode (many properties committed for clarity):

"contents" : {
      "data" : [
        {
          "type" : "type1",
          "id" : "6a406cdd7a9cace5"
        }, 
       {
          "type" : "type2",
          "id" : "ljhdgsouilghoipsu"
        }
     ]
 }

How can I correctly get my individual Type1 objects out of this structure?

6
  • It's probably because you're over-simplifying to make the example, but the example looks like it can be modelled as struct Item {type: String; id: String} and then struct Data {data: [Item]}, and then you could just decode data = try container.decode( Data.self, forKey: .data). Can you clarify what you mean by heterogenous, and where this variablity will occur? Commented Nov 21, 2019 at 15:08
  • Yes, the Item struct is actually already another Codable object. The variability occurs in that depending on itemType (type1, type2, etc), instead of the object being an Item, it could be another type. So there will not only be struct Item { }, but actually struct Item2 {} and struct Item3 { } depending on the value of type (each item DOES have a 'type' property.) Commented Nov 21, 2019 at 15:15
  • Thought that might be the case. Are the key names the same within the various 'Items' (eg.. all id, as in the example) or do they vary too? Commented Nov 21, 2019 at 15:25
  • the objects are complex nested structures that share some common key names, but not all. They aren't completely interchangeable by any means Commented Nov 21, 2019 at 15:26
  • this has been bugging me all afternoon - but I think I've got it now. Answer below shortly... Commented Nov 21, 2019 at 18:58

3 Answers 3

3

I think the easy way to get around the heterogenous data is to use an enum as an interim type to wrap your various Item types (which all need to Codable):

To allow myself to test this I've changed your json slightly to give me more heterogenous data for testing. I've used:

let json = """
{
  "contents": {
    "data": [
      {
        "type": "type1",
        "id": "6a406cdd7a9cace5"
      },
      {
        "type": "type2",
        "dbl": 1.01
      },
      {
        "type": "type3",
        "int": 5
      }
    ]
  }
}

and then created the three final types represented by this json

struct Item1: Codable {
   let type: String
   let id: String
}
struct Item2: Codable {
   let type: String
   let dbl: Double
}
struct Item3: Codable {
   let type: String
   let int: Int
}

To allow decoding the multiple types in a type-safe way (as required by Codable) you need to use a single type that can represent (or wrap) the possible options. An enum with associated values works nicely for this

enum Interim {
  case type1 (Item1)
  case type2 (Item2)
  case type3 (Item3)
  case unknown  //to handle unexpected json structures
}

So far, so good, but then it gets slightly more complicated when it comes to creating the Interim from the JSON. It will need a CodingKey enum which represents all the possible keys for all the Item# types, and then it will need to decode the JSON linking all these keys to their respective types and data:

extension Interim: Decodable {
   private enum InterimKeys: String, CodingKey {
      case type
      case id
      case dbl
      case int
   }
   init(from decoder: Decoder) throws {
      let container = try decoder.container(keyedBy: InterimKeys.self)
      let type = try container.decode(String.self, forKey: .type)
      switch type {
      case "type1":
         let id = try container.decode(String.self, forKey: .id)
         let item = Item1(type: type, id: id)
         self = .type1(item)
      case "type2":
         let dbl = try container.decode(Double.self, forKey: .dbl)
         let item = Item2(type: type, dbl: dbl)
         self = .type2(item)
      case "type3":
         let int = try container.decode(Int.self, forKey: .int)
         let item = Item3(type: type, int: int)
         self = .type3(item)
      default: self = .unknown
      }
   }
}

This provides the mechanism for decoding the heterogenous components, now we just need to deal with the higher-level keys. As we have a Decodable Interim type this is straightforward:

struct DataArray: Decodable {
   var data: [Interim]
}

struct Contents: Decodable {
   var contents: DataArray
}

This now means the json can be decoded like this...

let data = Data(json.utf8)
let decoder = JSONDecoder()
do {
    let contents = try decoder.decode(Contents.self, from: data)
    print(contents)
} catch {
    print("Failed to decode JSON")
    print(error.localizedDescription)
}

This successfully decodes the data into a nested structure where the major component is the array of Interim types with their associated Item# objects. The above produces the following output, showing these nested types:

Contents(contents: testbed.DataArray(data: [testbed.Interim.type1(testbed.Item1(type: "type1", id: "6a406cdd7a9cace5")), testbed.Interim.type2(testbed.Item2(type: "type2", dbl: 1.01)), testbed.Interim.type3(testbed.Item3(type: "type3", int: 5))]))

I think there should be an even safer way to do this with Type Erasure to provide a more extensible solution, but I've not got my head around that fully yet.

Sign up to request clarification or add additional context in comments.

Comments

0

I think you need use this structure:

struct A: Codable {
let contents: B?

enum CodingKeys: String, CodingKey {
    case contents
}

struct B: Codable {
    let data: [C]?

    enum CodingKeys: String, CodingKey {
        case data
    }

    struct C: Codable {
        let type : String?
        let id : String?
    }

}

}

extension A {

init(from decoder: Decoder) throws {

    let container = try decoder.container(keyedBy: CodingKeys.self)

    let contents = try container.decodeIfPresent(B.self, forKey: .contents)

    self.init(contents: contents)
}

}

Comments

0

I'd like to add to flanker's answer an improvement for cleaner approach to avoid having all possible keys to be stored under Interim's CodingKey. Here is an updated Interim

enum Interim: Decodable {

    case item1(Item1)
    case item2(Item2)
    case item3(Item3)
    case unknown

    init(from decoder: Decoder) throws {
        let typeContainer = try decoder.container(keyedBy: Key.self)

        // Fallback for any unsupported types
        guard let type = try? typeContainer.decode(ItemType.self, forKey: .type) else {
            self = .unknown
            return
        }

        // Let corresponding Decodable Item to be initialized from the same decoder.
        switch type {
            case .type1: self = .item1(try .init(from: decoder))
            case .type2: self = .item2(try .init(from: decoder))
            case .type3: self = .item3(try .init(from: decoder))
        }
    }

    /// These are values for Item.type
    private enum ItemType: String, Decodable {
        case type1
        case type2
        case type3
    }

    private enum Key: CodingKey {
        case type
    }
}

Comments

Your Answer

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