【问题标题】:How to decode JSON object with any number of key–value pairs in Swift?如何在 Swift 中使用任意数量的键值对解码 JSON 对象?
【发布时间】:2020-11-03 23:48:34
【问题描述】:

我正在使用 Swift 尝试解码 JSON:API 格式的 JSON 结果。我尝试解析的 JSON 具有如下形状:

{
    "data": {
        "type": "video",
        "id": "1",
        "attributes": {
            "name": "Test Video",
            "duration": 1234
        }
    }
}

我正在尝试创建一个对这些 JSON 对象进行编码的 Swift 结构,但我遇到了 attributes 键的问题,因为它可能包含任意数量的属性。

我试图将上面的 Swift 结构编码成如下所示:

struct JSONAPIMultipleResourceResponse: Decodable {
    var data: [JSONAPIResource]
}
struct JSONAPIResource: Decodable {
    var type: String
    var id: String
    var attributes: [String, String]?
}

typeid 属性应该出现在每个 JSON:API 结果中。 attributes 键应该是任意数量的键值对的列表;键和值都应该是字符串。

然后我尝试从这样的 API 解码 JSON 响应:

let response = try! JSONDecoder().decode(JSONAPIMultipleResourceResponse.self, from: data!)

如果我将typeid 属性保留在我的JSONAPIResource Swift 结构中,则上述方法有效,但是一旦我尝试对attributes 执行任何操作,我就会收到以下错误:

Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.valueNotFound(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "attributes", intValue: nil), _JSONKey(stringValue: "poster_path", intValue: nil)], debugDescription: "Expected String but found null value instead.", underlyingError: nil)): file /Users/[User]/Developer/[App]/LatestVideosQuery.swift, line 35  
2020-07-14 16:13:08.083721+0100 [App][57157:6135473] Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.valueNotFound(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "attributes", intValue: nil), _JSONKey(stringValue: "poster_path", intValue: nil)], debugDescription: "Expected String but found null value instead.", underlyingError: nil)): file /Users/[User]/Developer/[App]/LatestVideosQuery.swift, line 35

我知道 Swift 是非常强类型的,但我不确定如何在 Swift 中编码这些非结构化数据。我的计划是从我的 API 返回资源的通用 JSONAPIResource 表示,然后我可以映射到我的 Swift 应用程序中的模型结构,即将上述 JSON:API 资源转换为 Video 结构。

另外,我试图将attributes 对象中的值天真地转换为字符串,但正如您所见,duration 是一个 整数 值。如果有办法让 attributes 在我的 JSONAPIResource 结构中保留原始值,例如整数和布尔值,而不仅仅是字符串,我会很想知道如何!

【问题讨论】:

  • “任意数量的键值对的列表”。你知道所有可能的值吗?
  • @LeoDabus 不是预先设置,因为属性会根据资源的type 而变化。如果可能的话,我基本上只想要一个键和值的“包”。
  • @MartinBean...您希望最终模型是什么? “一袋键/值”是一个字典,但正如您所见,如果您不能确保值的类型相同,那么您就不能解码它
  • attributes 字典不是 [String:String],因为 duration 的值是 Int。查看type 值和对应的attributes 字典之间是否存在一致的依赖关系。如果有一个使用关联类型和一堆不同结构的枚举。
  • @vadian 我知道。我在帖子的末尾注意到了这一点。

标签: json swift codable decodable


【解决方案1】:

如果它是一个完全通用的键/值包(这可能表明需要进行可能的设计更改),您可以创建一个 enum 来保存 JSON 可以保存的不同(原始)值:

enum JSONValue: Decodable {
   case number(Double)
   case integer(Int)
   case string(String)
   case bool(Bool)
   case null

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

      if let int = try? container.decode(Int.self) {
         self = .integer(int)
      } else if let double = try? container.decode(Double.self) {
         self = .number(double)
      } else if let string = try? container.decode(String.self) {
         self = .string(string)
      } else if let bool = try? container.decode(Bool.self) {
         self = .bool(bool)
      } else if container.decodeNil() {
         self = .null
      } else {
        // throw some DecodingError
      }
   }
}

然后您可以将attributes 设置为:

var attributes: [String: JSONValue]

【讨论】:

    【解决方案2】:

    如果type 值与attributes 中的键值对之间存在一致的关系,我建议将attributes 声明为具有关联类型的枚举,并根据type 值解码类型。

    例如

    let jsonString = """
    {
        "data": {
            "type": "video",
            "id": "1",
            "attributes": {
                "name": "Test Video",
                "duration": 1234
            }
        }
    }
    """
    
    enum ResourceType : String, Decodable {
       case video
    }
    
    enum AttributeType {
        case video(Video)
    }
    
    struct Video : Decodable {
        let name : String
        let duration : Int
    }
    
    struct Root : Decodable {
        let data : Resource
    }
    
    struct Resource : Decodable {
        let type : ResourceType
        let id : String
        let attributes : AttributeType
        
        private enum CodingKeys : String, CodingKey { case type, id, attributes }
        
        init(from decoder : Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
            self.type = try container.decode(ResourceType.self, forKey: .type)
            self.id = try container.decode(String.self, forKey: .id)
            switch self.type {
                case .video:
                    let videoAttributes = try container.decode(Video.self, forKey: .attributes)
                    attributes = .video(videoAttributes)
            }
        }
    }
    
    let data = Data(jsonString.utf8)
    
    do {
        let result = try JSONDecoder().decode(Root.self, from: data)
        print(result)
    } catch {
        print(error)
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-12-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-24
      • 1970-01-01
      相关资源
      最近更新 更多