6

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

Ganesh Somani
  • 2,280
  • 2
  • 28
  • 37

3 Answers3

8

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
David Siegel
  • 1,604
  • 11
  • 13
  • I am just going through this, and trying to understand how we can access value from `configData: [ConfigDatumElement]` – Ganesh Somani Feb 28 '18 at 06:19
  • 1
    I've added a working code sample – you could iterate over that array, and use a switch statement as I've demonstrated. – David Siegel Feb 28 '18 at 06:27
2

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!

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Updated my question. Basic thing is the array can contain any kind of data – Ganesh Somani Jan 18 '18 at 07:35
  • @GaneshSomani As I said at the beginning, what do you want to do with the JSON if it can be _anything_? What are you trying to ultimately do? – Sweeper Jan 18 '18 at 07:41
  • This is some excess info which supports my main data... Like let say I am storing a count, so it can be the initial starting count which I can get from server. Or this is extra info for music data, then it can contain URL for album covers – Ganesh Somani Jan 18 '18 at 07:42
  • @GaneshSomani So you do know what you are getting. You can tell from the "main data" right? Then create a struct for each of those situations. And decode using the correct struct type. Or else you'll have to convert the JSON into a dictionary using the Swift 3 way. – Sweeper Jan 18 '18 at 07:45
0

You are trying to JSON to object or object to JSON ? you can try this code add any swift file:

extension String {
    var xl_json: Any? {
        if let data = data(using: String.Encoding.utf8) {
            return try? JSONSerialization.jsonObject(with: data, options: .mutableContainers)
        }
        return nil
    }
}

extension Array {
    var xl_json: String? {
        guard let data = try? JSONSerialization.data(withJSONObject: self, options: []) else {
            return nil
        }
        return String(data: data, encoding: .utf8)
    }
}

extension Dictionary {
    var xl_json: String? {
        guard let data = try? JSONSerialization.data(withJSONObject: self, options: []) else {
            return nil
        }
        return String(data: data, encoding: .utf8)
    }
}

and run this code:

let str = "{\"key\": \"Value\"}"
let dict = str.xl_json as! [String: String] // JSON to Objc
let json = dict.xl_json                     // Objc to JSON

print("jsonStr - \(str)")
print("objc - \(dict)")
print("jsonStr - \(json ?? "nil")")

Finally, you'll get it:

jsonStr - {"key": "Value"}
objc - ["key": "Value"]
jsonStr - {"key":"Value"}
xx11dragon
  • 166
  • 1
  • 10
  • JSONSerialization would have been always easier to perform, but with Swift 4 and JSONDecoder, it is something I am facing issues with – Ganesh Somani Jan 18 '18 at 06:33