Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 348
Ganesh Somani Avatar asked Jan 18 '18 05:01

Ganesh Somani


2 Answers

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
like image 99
David Siegel Avatar answered Nov 07 '22 16:11

David Siegel


You first need to decide what to do if the second JSON comes up. The second JSON format has way less info. What do you want to do with those data (config_title) that you lost? Do you actually need them at all?

If you do need to store the config_titles if they are present, then I suggest you to create a ConfigItem struct, which looks like this:

struct ConfigItem: Codable {
    let name: String
    let configTitle: String?

    init(name: String, configTitle: String? = nil) {
        self.name = name
        self.configTitle = configTitle
    }

    // encode and init(decoder:) here...
    // ...
}

Implement the required encode and init(decoder:) methods. You know the drill.

Now, when you are decoding your JSON, decode the config_data key as usual. But this time, instead of using an [Any], you can decode to [ConfigItem]! Obviously this won't always work because the JSON can sometimes be in the second form. So you catch any error thrown from that and decode config_data using [String] instead. Then, map the string array to a bunch of ConfigItems!

like image 26
Sweeper Avatar answered Nov 07 '22 17:11

Sweeper