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

岁酱吖の 提交于 2019-12-05 21:21:32

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

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!

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"}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!