带有可配置键的 Swift 4 JSON 解码

Posted

技术标签:

【中文标题】带有可配置键的 Swift 4 JSON 解码【英文标题】:Swift 4 JSON decode with configurable keys 【发布时间】:2018-02-16 18:17:14 【问题描述】:

我是 Swift 新手,我需要使用一些可配置键来解析 JSON。 与我在这里看到的许多示例相反,密钥在解码操作开始之前是已知的,它们仅取决于传递给端点的一些参数。

例子:

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

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

将分别回答:

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

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

鉴于这些密钥在解码之前是已知的,我希望能够巧妙地声明 Decodable 结构和/或 CodingKeys,无需编写

init(from decoder: Decoder)

很遗憾,我无法提出这样的声明。

当然我不想为每个可能的参数值写一个 Decodable/CodingKeys 结构:-)

有什么建议吗?

【问题讨论】:

Flattening JSON when keys are known only at runtime的可能重复 不完全一样,但差不多。建议的解决方案将适用于您的数据。 (顺便说一句,jeys 应该是 fixed_key1 & fixed_key2 嗨内森。感谢您突出显示 fixed_key1 和 2 上的错字。现在已编辑。关于您链接中建议的解决方案,它看起来不正确。在这种情况下,您将丢弃幻像(变量键),而我想保留它们,因此它们需要在我的结构中。无论如何都试图调整 y 代码,但是在尝试解码时,我得到了类型不匹配异常“type = Dictionary, context = Context(codingPath: [], debugDescription: "Expected to decode Dictionary 但发现而是一个数组。”,基础错误:无)“。无论我是否在我的结构中有键(作为可选字符串)。 【参考方案1】:

除非您的所有 JSON 键都是编译时常量,否则编译器无法合成解码方法。但是您可以采取一些措施来减少手动解码的麻烦。

首先,一些辅助结构和扩展:

/*
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 

然后手动解码:

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)
        
    

最后是你如何使用它:

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)

【讨论】:

嗨,代码不同,我刚刚解决了使用/调整来自Swift 4 Decodable with keys not known until decoding time 的代码。但是,您在这里的回答看起来更优雅。猜猜这个话题很热门,答案正在发展/改进。非常感谢! 在回答了另一个问题后,我实际上将GenericCodingKeys 保留为Xcode 中的sn-p 并不断改进它。这就是它如何到达现在的位置。【参考方案2】:

采用与@Code Different's answer 类似的方法,您可以通过解码器的userInfo 字典传递给定的参数信息,然后将其传递给您用于从密钥容器中解码的密钥类型。

首先,我们可以在CodingUserInfoKey 上定义一个新的静态成员,用作userInfo 字典中的键:

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

(强制解包永远不会失败;我认为初始化程序是失败的as a bug)。

然后我们可以为您的端点参数定义一个类型,再次使用静态成员来抽象出底层字符串:

// 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 

然后我们可以定义你的数据模型类型:

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

然后像这样Decodable

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

您可以看到我们在CodingKeys 上使用静态属性定义固定键,而对于可变键,我们使用将给定参数作为参数的静态方法。

现在您可以像这样执行解码:

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

【讨论】:

也很棒!谢谢哈米什。我不是特别喜欢 EndpointParameter 结构的定义,因为参数可以采用 许多 值,但这很容易适应。 @MarcoD。当然,你没有展示你的具体用例,所以这有点猜测;我的主要观点是使用强类型而不是字符串。很高兴它很有用:)

以上是关于带有可配置键的 Swift 4 JSON 解码的主要内容,如果未能解决你的问题,请参考以下文章

Swift 4 可解码,直到解码时才知道密钥

带有嵌套数据的 Swift 4 JSON 解码器

使用可选值 Swift 4 解码嵌套 JSON

Swift 4 可解码的 json 数组

Swift 4 可解码 - 附加变量

如何在 Swift 4 中为 JSON 编写可解码,其中键是动态的?