我正在使用Swift尝试解码JSON:API格式的JSON结果。我试图解析的JSON的形状如下:
{
"data": {
"type": "video",
"id": "1",
"attributes": {
"name": "Test Video",
"duration": 1234
}
}
}
我正试图创建一个Swift结构来编码这些JSON对象,但我对attributes
键有问题,因为它可能包含任何数量的属性。
我试图将上面的Swift结构编码成这样:
struct JSONAPIMultipleResourceResponse: Decodable {
var data: [JSONAPIResource]
}
struct JSONAPIResource: Decodable {
var type: String
var id: String
var attributes: [String, String]?
}
type
和id
属性应出现在每个JSON:API结果中。attributes
密钥应该是任意数量的密钥-值对的列表;键和值都应该是字符串。
然后我尝试解码API的JSON响应,如下所示:
let response = try! JSONDecoder().decode(JSONAPIMultipleResourceResponse.self, from: data!)
如果我在JSONAPIResource
Swift结构中保留type
和id
属性,则上述操作有效,但一旦我尝试对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
是一个整数值。如果有一种方法可以让我的JSONAPIResource
结构中的attributes
保留诸如整数和布尔值之类的基元值,而不仅仅是字符串,我会很想看看如何!
如果它是一个完全通用的键/值包(这可能表明需要进行可能的设计更改(,则可以创建一个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]
如果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)
}