3

I'm new to Swift and I need to parse a JSON with some configurable keys. Opposite to many examples I've seen here, the keys are known before the decode operation is started, they just depend on some parameters passed to endpoint.

Example:

https://some.provider.com/endpoint/?param=XXX

and

https://some.provider.com/endpoint/?param=YYY

will answer, respectively:

[
    {
        "fixed_key1": "value1",
        "fixed_key2": "value2",
        "variable_key_1_XXX": "some value",
        "variable_key_2_XXX": "some other value"
    },
    ...
]      

and

[
    {
        "fixed_key1": "value1",
        "fixed_key2": "value2",
        "variable_key_1_YYY": "some value",
        "variable_key_2_YYY": "some other value"
    },
    ...
]  

Given that those keys are known before decoding, I was hoping to get away with some clever declaration of a Decodable structure and/or CodingKeys, without the need to write the

init(from decoder: Decoder)

Unfortunately, I was not able to come up with such a declaration.

Of course I don't want to write one Decodable/CodingKeys structure for every possible parameter value :-)

Any suggestion ?

Hamish
  • 78,605
  • 19
  • 187
  • 280
Marco D.
  • 33
  • 5
  • Possible duplicate of [Flattening JSON when keys are known only at runtime](https://stackoverflow.com/questions/45666668/flattening-json-when-keys-are-known-only-at-runtime) – nathan Feb 17 '18 at 01:02
  • Not exactly the same but pretty much. The proposed solutions will work with your data. (btw jeys should be `fixed_key1` & `fixed_key2`) – nathan Feb 17 '18 at 01:03
  • Hi nathan. thanks for highlighting the typo on fixed_key1 and 2. Edited now. About the proposed solution in your link, it doesn't look right. In that case you are discarding the phantom (variable keys), while i want to keep them, so they need to be in my struct. Tried to adapt y code anyway but when trying to decode i got the Type mismatch exception "type = Dictionary, context = Context(codingPath: [], debugDescription: "Expected to decode Dictionary but found an array instead.", underlyingError: nil)". both if i have the keys in my struct (as optional String) or not. – Marco D. Feb 17 '18 at 10:40

2 Answers2

2

Unless all your JSON keys are compile-time constants, the compiler can't synthesize the decoding methods. But there are a few things you can do to make manual decoding a lot less cumbersome.

First, some helper structs and extensions:

/*
Allow us to initialize a `CodingUserInfoKey` with a `String` so that we can write:
    decoder.userInfo = ["param": "XXX"]

Instead of:
    decoder.userInfo = [CodingUserInfoKey(rawValue:"param")!: "XXX"]
*/
extension CodingUserInfoKey: ExpressibleByStringLiteral {
    public typealias StringLiteralType = String

    public init(stringLiteral value: StringLiteralType) {
        self.rawValue = value
    }
}

/*
This struct is a plain-vanilla implementation of the `CodingKey` protocol. Adding
`ExpressibleByStringLiteral` allows us to initialize a new instance of
`GenericCodingKeys` with a `String` literal, for example:
    try container.decode(String.self, forKey: "fixed_key1")

Instead of:
    try container.decode(String.self, forKey: GenericCodingKeys(stringValue: "fixed_key1")!)
*/
struct GenericCodingKeys: CodingKey, ExpressibleByStringLiteral {
    // MARK: CodingKey
    var stringValue: String
    var intValue: Int?

    init?(stringValue: String) { self.stringValue = stringValue }
    init?(intValue: Int) { return nil }

    // MARK: ExpressibleByStringLiteral
    typealias StringLiteralType = String
    init(stringLiteral: StringLiteralType) { self.stringValue = stringLiteral }
}

Then the manual decoding:

struct MyDataModel: Decodable {
    var fixedKey1: String
    var fixedKey2: String
    var variableKey1: String
    var variableKey2: String

    enum DecodingError: Error {
        case missingParamKey
        case unrecognizedParamValue(String)
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: GenericCodingKeys.self)

        // Decode the fixed keys
        self.fixedKey1 = try container.decode(String.self, forKey: "fixed_key1")
        self.fixedKey2 = try container.decode(String.self, forKey: "fixed_key2")

        // Now decode the variable keys
        guard let paramValue = decoder.userInfo["param"] as? String else {
            throw DecodingError.missingParamKey
        }

        switch paramValue {
        case "XXX":
            self.variableKey1 = try container.decode(String.self, forKey: "variable_key_1_XXX")
            self.variableKey2 = try container.decode(String.self, forKey: "variable_key_2_XXX")
        case "YYY":
            self.variableKey1 = try container.decode(String.self, forKey: "variable_key_1_YYY")
            self.variableKey2 = try container.decode(String.self, forKey: "variable_key_2_YYY")
        default:
            throw DecodingError.unrecognizedParamValue(paramValue)
        }
    }
}

And finally here's how you use it:

let jsonData = """
[
    {
        "fixed_key1": "value1",
        "fixed_key2": "value2",
        "variable_key_1_XXX": "some value",
        "variable_key_2_XXX": "some other value"
    }
]
""".data(using: .utf8)!

// Supplying the `userInfo` dictionary is how you "configure" the JSON-decoding 
let decoder = JSONDecoder()
decoder.userInfo = ["param": "XXX"]
let model = try decoder.decode([MyDataModel].self, from: jsonData)

print(model)
Code Different
  • 90,614
  • 16
  • 144
  • 163
  • Hi Code Different, I just solved using/adaptng your code from [Swift 4 Decodable with keys not known until decoding time](https://stackoverflow.com/questions/45598461/swift-4-decodable-with-keys-not-known-until-decoding-time). Your answer here however, looks more elegant. Guess the topic is hot and answers are evolving/improving. Thank you very much! – Marco D. Feb 17 '18 at 15:47
  • 1
    After answering the other question, I actually kept `GenericCodingKeys` as a snippet in Xcode and kept improving on it. That's how it arrived at where it is now. – Code Different Feb 17 '18 at 17:36
0

Taking a similar approach to @Code Different's answer, you can pass the given parameter information through the decoder's userInfo dictionary, and then pass this onto the key type that you use to decode from the keyed container.

First, we can define a new static member on CodingUserInfoKey to use as the key in the userInfo dictionary:

extension CodingUserInfoKey {
  static let endPointParameter = CodingUserInfoKey(
    rawValue: "com.yourapp.endPointParameter"
  )!
}

(the force unwrap never fails; I regard the fact the initialiser is failable as a bug).

Then we can define a type for your endpoint parameter, again using static members to abstract away the underlying strings:

// You'll probably want to rename this to something more appropriate for your use case
// (same for the .endPointParameter CodingUserInfoKey).
struct EndpointParameter {

  static let xxx = EndpointParameter("XXX")
  static let yyy = EndpointParameter("YYY")
  // ...

  var stringValue: String
  init(_ stringValue: String) { self.stringValue = stringValue }
}

Then we can define your data model type:

struct MyDataModel {
  var fixedKey1: String
  var fixedKey2: String
  var variableKey1: String
  var variableKey2: String
}

And then make it Decodable like so:

extension MyDataModel : Decodable {

  private struct CodingKeys : CodingKey {
    static let fixedKey1 = CodingKeys("fixed_key1")
    static let fixedKey2 = CodingKeys("fixed_key2")

    static func variableKey1(_ param: EndpointParameter) -> CodingKeys {
      return CodingKeys("variable_key_1_\(param.stringValue)")
    }
    static func variableKey2(_ param: EndpointParameter) -> CodingKeys {
      return CodingKeys("variable_key_2_\(param.stringValue)")
    }

    // We're decoding an object, so only accept String keys.
    var stringValue: String
    var intValue: Int? { return nil }
    init?(intValue: Int) { return nil }
    init(stringValue: String) { self.stringValue = stringValue }
    init(_ stringValue: String) { self.stringValue = stringValue }
  }

  init(from decoder: Decoder) throws {
    guard let param = decoder.userInfo[.endPointParameter] as? EndpointParameter else {
      // Feel free to make this a more detailed error.
      struct EndpointParameterNotSetError : Error {}
      throw EndpointParameterNotSetError()
    }

    let container = try decoder.container(keyedBy: CodingKeys.self)

    self.fixedKey1 = try container.decode(String.self, forKey: .fixedKey1)
    self.fixedKey2 = try container.decode(String.self, forKey: .fixedKey2)
    self.variableKey1 = try container.decode(String.self, forKey: .variableKey1(param))
    self.variableKey2 = try container.decode(String.self, forKey: .variableKey2(param))
  }
}

You can see we're defining the fixed keys using static properties on CodingKeys, and for the variable keys we're using static methods that take the given parameter as an argument.

Now you can perform a decode like so:

let jsonString = """
[
  {
    "fixed_key1": "value1",
    "fixed_key2": "value2",
    "variable_key_1_XXX": "some value",
    "variable_key_2_XXX": "some other value"
  }
]
"""

let decoder = JSONDecoder()
decoder.userInfo[.endPointParameter] = EndpointParameter.xxx
do {
  let model = try decoder.decode([MyDataModel].self, from: Data(jsonString.utf8))
  print(model)
} catch {
  print(error)
}

// [MyDataModel(fixedKey1: "foo", fixedKey2: "bar",
//              variableKey1: "baz", variableKey2: "qux")]
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • Also great!. thank you Hamish. I don't particularly like the definition of **EndpointParameter** struct since the parameter can assume _many_ values, but this can easily be adapted. – Marco D. Feb 18 '18 at 09:26
  • @MarcoD. Sure, you didn't show your concrete use case so it was a bit of a guess; the main point I was making was the use of strong types rather than strings. Glad it was useful :) – Hamish Feb 18 '18 at 11:37