1

Need some help with more complicated json, with the newest swift4.1 encoder/decoder:

struct:

struct LMSRequest: Decodable {
let id : Int?
let method : String?
let params : [String]?
enum CodingKeys: String, CodingKey {
    case id = "id"
    case method = "method"
    case params = "params"
}
init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    id = try values.decodeIfPresent(Int.self, forKey: .id)
    method = try values.decodeIfPresent(String.self, forKey: .method)
    params = try values.decodeIfPresent([String].self, forKey: .params)
}}

json:

let json = """
{
  "id": 1,
  "method": "slim.request",
  "params": [
    "b8:27:eb:db:6d:62",
    [
      "serverstatus",
      "-",
      1,
      "tags:GPASIediqtymkovrfijnCYXRTIuwxNlasc"
    ]
  ]
}
""".data(using: .utf8)!

code:

let decoder = JSONDecoder()
let lms = try decoder.decode(LMSRequest.self, from: json)
print(lms)

Error is expected to decode string but found array instead. It's coming from the nested array within the "params" array... really stuck on how to build this out, Thanks!

pkamb
  • 33,281
  • 23
  • 160
  • 191
Scott Kramer
  • 1,711
  • 3
  • 24
  • 37
  • `params` is a `[String]?`, but as you note it includes both a string and a nested array of strings. What do you want he final `params` to look like? Should it flatten them or something else? There is also a `1` in the middle of the nested data array, which is not a String. Do you want this converted to a String `"1"` or something else? – Rob Napier Jun 10 '18 at 02:54
  • Looking at it-- an array of objects-- since there's an int in there. Need to get a nested array going 1st though. – Scott Kramer Jun 10 '18 at 02:58
  • An "array of objects" doesn't really mean much. Do you mean there could be `Cat` objects in there? `House` objects? Do you really mean "absolutely any object at all, even ones that can't be expressed in JSON?" Or do you mean "a small list of types, probably just strings and ints" in which case you mean an enum. Or, looking at this, I suspect it is really meant to be a all strings and want conversions. But you'll need to work out your Swift type first, and then we can decode into it. – Rob Napier Jun 10 '18 at 03:27
  • strings & ints- – Scott Kramer Jun 10 '18 at 03:32

1 Answers1

3

Given what you've described, you should store params as an enum like this:

enum Param: CustomStringConvertible {
    case string(String)
    case int(Int)
    case array([Param])

    var description: String {
        switch self {
        case let .string(string): return string
        case let .int(int): return "\(int)"
        case let .array(array): return "\(array)"
        }
    }
}

A param can either be a string, an int, or an array of more params.

Next, you can make Param Decodable by trying each option in turn:

extension Param: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let string = try? container.decode(String.self) {
            self = .string(string)
        } else if let int = try? container.decode(Int.self) {
            self = .int(int)
        } else {
            self = .array(try container.decode([Param].self))
        }
    }
}

Given this, there's no need for custom decoding logic in LMSRequest:

struct LMSRequest: Decodable {
    let id : Int?
    let method : String?
    let params : [Param]?
}

As a side note, I would carefully consider whether these fields are all truly optional. It's very surprising that id is optional, and quite surprising that method is optional, and slightly surprising that params are optional. If they're not really optional, don't make them optional in the type.


From your comments, you're probably misunderstanding how to access enums. params[1] is not a [Param]. It's an .array([Param]). So you have to pattern match it since it might have been a string or an int.

if case let .array(values) = lms.params[1] { print(values[0]) }

That said, if you're doing this a lot, you can make this simpler with extensions on Param:

extension Param {
    var stringValue: String? { if case let .string(value) = self { return value } else { return nil } }
    var intValue: Int? { if case let .int(value) = self { return value } else { return nil } }
    var arrayValue: [Param]? { if case let .array(value) = self { return value } else { return nil } }

    subscript(_ index: Int) -> Param? {
        return arrayValue?[index]
    }
}

With that, you can say things like:

let serverstatus: String? = lms.params[1][0]?.stringValue

Which is probably closer to what you had in mind. (The : String? is just to be clear about the returned type; it's not required.)

For a more complex and worked-out example of this approach, see my generic JSON Decodable that this is a subset of.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610