Swift 4 JSON decode with configurable keys

安稳与你 提交于 2019-12-06 06:51:31

问题


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 ?


回答1:


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)



回答2:


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")]


来源:https://stackoverflow.com/questions/48832709/swift-4-json-decode-with-configurable-keys

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