Search code examples
swiftswift4jsondecoder

Swift Codable - Parse JSON array which can contain different data type


I am trying to parse a JSON array which can be

{
  "config_data": [
      {
        "name": "illuminate",
        "config_title": "Blink"
      },
      {
        "name": "shoot",
        "config_title": "Fire"
      }
    ]
}

or it can be of following type

{
  "config_data": [
          "illuminate",
          "shoot"
        ]
}

or even

{
    "config_data": [
              25,
              100
            ]
  }

So to parse this using JSONDecoder I created a struct as follows -

Struct Model: Codable {
  var config_data: [Any]?

  enum CodingKeys: String, CodingKey {
    case config_data = "config_data"
   }

  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    config_data = try values.decode([Any].self, forKey: .config_data)
  }
}

But this would not work since Any does not confirm to decodable protocol. What could be the solution for this. The array can contain any kind of data


Solution

  • I used quicktype to infer the type of config_data and it suggested an enum with separate cases for your object, string, and integer values:

    struct ConfigData {
        let configData: [ConfigDatumElement]
    }
    
    enum ConfigDatumElement {
        case configDatumClass(ConfigDatumClass)
        case integer(Int)
        case string(String)
    }
    
    struct ConfigDatumClass {
        let name, configTitle: String
    }
    

    Here's the complete code example. It's a bit tricky to decode the enum but quicktype helps you out there:

    // To parse the JSON, add this file to your project and do:
    //
    //   let configData = try? JSONDecoder().decode(ConfigData.self, from: jsonData)
    
    import Foundation
    
    struct ConfigData: Codable {
        let configData: [ConfigDatumElement]
    
        enum CodingKeys: String, CodingKey {
            case configData = "config_data"
        }
    }
    
    enum ConfigDatumElement: Codable {
        case configDatumClass(ConfigDatumClass)
        case integer(Int)
        case string(String)
    
        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            if let x = try? container.decode(Int.self) {
                self = .integer(x)
                return
            }
            if let x = try? container.decode(String.self) {
                self = .string(x)
                return
            }
            if let x = try? container.decode(ConfigDatumClass.self) {
                self = .configDatumClass(x)
                return
            }
            throw DecodingError.typeMismatch(ConfigDatumElement.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for ConfigDatumElement"))
        }
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            switch self {
            case .configDatumClass(let x):
                try container.encode(x)
            case .integer(let x):
                try container.encode(x)
            case .string(let x):
                try container.encode(x)
            }
        }
    }
    
    struct ConfigDatumClass: Codable {
        let name, configTitle: String
    
        enum CodingKeys: String, CodingKey {
            case name
            case configTitle = "config_title"
        }
    }
    

    It's nice to use the enum because you get the most type-safety that way. The other answers seem to lose this.

    Using quicktype's convenience initializers option, a working code sample is:

    let data = try ConfigData("""
    {
      "config_data": [
        {
          "name": "illuminate",
          "config_title": "Blink"
        },
        {
          "name": "shoot",
          "config_title": "Fire"
        },
        "illuminate",
        "shoot",
        25,
        100
      ]
    }
    """)
    
    for item in data.configData {
        switch item {
        case .configDatumClass(let d):
            print("It's a class:", d)
        case .integer(let i):
            print("It's an int:", i)
        case .string(let s):
            print("It's a string:", s)
        }
    }
    

    This prints:

    It's a class: ConfigDatumClass(name: "illuminate", configTitle: "Blink")
    It's a class: ConfigDatumClass(name: "shoot", configTitle: "Fire")
    It's a string: illuminate
    It's a string: shoot
    It's an int: 25
    It's an int: 100