Home > database >  Decoding JSON in Swift with Mixed Types and Mixed Keyed/Unkeyed
Decoding JSON in Swift with Mixed Types and Mixed Keyed/Unkeyed

Time:12-07

I'm struggling to decode a JSON structure in Swift 5, which looks like the simplified example below. There are two issues I'm struggling with. The outer array is unkeyed, and the inner array is keyed. On top of that the inner array contains occasional arrays of mixed type String and Int. I could provide a couple dozen things that didn't work at all, but I'll just provide the JSON:

[
  12,
  {
    "a": [
      "orange",
      10,
      "purple"
    ],
    "b": [
      "red",
      9,
      "blue
    ],
    "c": [
      "yellow",
      "green"
    ]
  },
  "string one",
  "string two"
]

Any ideas are appreciated.

CodePudding user response:

A possible solution usable in Playground:

func heterogenousJSON() {
    let jsonStr = """
        [
        12,
        {
        "a": [
        "orange",
        10,
        "purple"
        ],
        "b": [
        "red",
        9,
        "blue"
        ],
        "c": [
        "yellow",
        "green"
        ]
        },
        "string one",
        "string two"
        ]
        """

    struct CodableStruct: Codable, CustomStringConvertible {
        let a: [CodableStructValues]
        let b: [CodableStructValues]
        let c: [String] //Here I set it as [String], but could be indeed [CodableStructValues], just to make it more "usual case"

        var description: String {
            "{ \"a\": [\(a.map{ $0.description }.joined(separator: ", "))] }\n"  
            "{ \"b\": [\(b.map{ $0.description }.joined(separator: ", "))] }\n"  
            "{ \"c\": [\(c.map{ $0 }.joined(separator: ", "))] }"
        }
    }

    enum CodableStructValues: Codable, CustomStringConvertible {
        case asInt(Int)
        case asString(String)

        init(from decoder: Decoder) throws {
            let values = try decoder.singleValueContainer()

            if let asInt = try? values.decode(Int.self) {
                self = .asInt(asInt)
                return
            }
            //For the next: we didn't use `try?` but try, and it will throw if it's not a String
            // We assume that if it wasn't okay from previous try?, it's the "last chance". It needs to be of this type, or it will throw an error
            let asString = try values.decode(String.self)
            self = .asString(asString)
        }

        var description: String {
            switch self {
            case .asInt(let intValue):
                return "\(intValue)"
            case .asString(let stringValue):
                return stringValue
            }
        }
    }

    enum Heterogenous: Codable {
        case asInt(Int)
        case asString(String)
        case asCodableStruct(CodableStruct)

        init(from decoder: Decoder) throws {
            let values = try decoder.singleValueContainer()
            if let asInt = try? values.decode(Int.self) {
                self = .asInt(asInt)
                return
            } else if let asString = try? values.decode(String.self) {
                self = .asString(asString)
                return
            }
            //For the next: we didn't use `try?` but try, and it will throw if it's not a String
            // We assume that if it wasn't okay from previous try?, it's the "last chance". It needs to be of this type, or it will throw an error
            let asStruct = try values.decode(CodableStruct.self)
            self = .asCodableStruct(asStruct)
        }
    }

    do {
        let json = Data(jsonStr.utf8)
        let parsed = try JSONDecoder().decode([Heterogenous].self, from: json)
        print(parsed)

        parsed.forEach { aHeterogenousParsedValue in
            switch aHeterogenousParsedValue {
            case .asInt(let intValue):
                print("Got Int: \(intValue)")
            case .asString(let stringValue):
                print("Got String: \(stringValue)")
            case .asCodableStruct(let codableStruct):
                print("Got Struct: \(codableStruct)")
            }
        }
    } catch {
        print("Error while decoding JSON: \(error)")
    }
}

heterogenousJSON()

The main idea is to use a Codable enum with associated values which will hold all the heterogenous values. You need then to have a custom init(from decoder: Decoder). I made the values Codable, but in fact I only really made the Decodable part. There is no override of the reverse.

I used CustomStringConvertible (and its description) to have a more readable prints.

I added a forEach() when printing parsed to show you how to handle afterwards the values. You can use if case let instead of a switch if you need only one case.

As said by @vadian in comments, having heterogenous values like that in an array is not a good practice. You said that in your case you can't change them with back-end dev, but I'm pointing it out in this answer in case of someone else having the same issue and if he/she could change it, for recommendation.

  • Related