处理包含多种类型的 JSON 数组 - Swift 4 Decodable
Posted
技术标签:
【中文标题】处理包含多种类型的 JSON 数组 - Swift 4 Decodable【英文标题】:Handling JSON Array Containing Multiple Types - Swift 4 Decodable 【发布时间】:2018-09-07 09:00:04 【问题描述】:我正在尝试使用 Swift 4 Decodable 来解析包含两种不同类型对象的数组。数据看起来像这样,included
数组是同时包含 Member
和 ImageMedium
对象的数组:
"data": [
"id": "8f7cbbac-c133-4b5e-a2ec-1f32353018fa",
"type": "post",
"title": "Test Post 1",
"owner-id": "8986563c-438c-4d77-8115-9e5de2b6e477",
"owner-type": "member"
,
"id": "f6b3c640-a58b-449f-93c7-f6cb7b569a9c",
"type": "post",
"title": "Test Post 2",
"owner-id": "38d845a4-db66-48b9-9c15-d857166e255e",
"owner-type": "member"
],
"included": [
"id": "8986563c-438c-4d77-8115-9e5de2b6e477",
"type": "member",
"first-name": "John",
"last-name": "Smith"
,
"id": "d7218ca1-de53-4832-bb8f-dbceb6747e98",
"type": "image-medium",
"asset-url": "https://faketest.com/fake-test-1.png",
"owner-id": "f6b3c640-a58b-449f-93c7-f6cb7b569a9c",
"owner-type": "post"
,
"id": "c59b8c72-13fc-44fd-8ef9-4b0f8fa486a0",
"type": "image-medium",
"asset-url": "https://faketest.com/fake-test-2.png",
"owner-id": "8f7cbbac-c133-4b5e-a2ec-1f32353018fa",
"owner-type": "post"
,
"id": "38d845a4-db66-48b9-9c15-d857166e255e",
"type": "member",
"first-name": "Jack",
"last-name": "Doe"
]
我已经尝试了很多不同的方法来使用 Decodable 干净地解决这个问题,但到目前为止,唯一对我有用的是为 Included
创建一个结构,其中包含两个对象的所有属性作为可选,就像这样:
struct Root: Decodable
let data: [Post]?
let included: [Included]?
struct Post: Decodable
let id: String?
let type: String?
let title: String?
let ownerId: String?
let ownerType: String?
enum CodingKeys: String, CodingKey
case id
case type
case title
case ownerId = "owner-id"
case ownerType = "owner-type"
struct Included: Decodable
let id: String?
let type: String?
let assetUrl: String?
let ownerId: String?
let ownerType: String?
let firstName: String?
let lastName: String?
enum CodingKeys: String, CodingKey
case id
case type
case assetUrl = "asset-url"
case ownerId = "owner-id"
case ownerType = "owner-type"
case firstName = "first-name"
case lastName = "last-name"
这可以通过实现一种方法来实现,该方法根据其type
属性从Included
结构创建Member
和ImageMedium
对象,但它显然不太理想。我希望有一种方法可以使用自定义init(from decoder: Decoder)
来实现这一点,但我还没有让它工作。有什么想法吗?
【问题讨论】:
当然,您应该使用带有这些类型关联值的Codable
枚举来处理它。
@user28434 你能详细说明一下吗?如果有帮助,我编辑了问题以包含 Root
和 Post
结构。
链接:***.com/questions/48314724/…
【参考方案1】:
我的建议是对所有项目使用单一类型Post
。为了区分不同的类型,将type
键解码为枚举,并根据情况对属性进行解码。
这需要将所有非全局属性声明为var
。
struct Root : Decodable
let data : [Post]
let included : [Post]
enum PostType : String, Decodable
case member, post, imageMedium = "image-medium"
struct Post : Decodable
let id: String
let type: PostType
var title: String?
var assetUrl: String?
var ownerId: String?
var ownerType: String?
var firstName: String?
var lastName: String?
enum CodingKeys: String, CodingKey
case id, type, title
case assetUrl = "asset-url"
case ownerId = "owner-id"
case ownerType = "owner-type"
case firstName = "first-name"
case lastName = "last-name"
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
type = try container.decode(PostType.self, forKey: .type)
switch type
case .member:
firstName = try container.decode(String.self, forKey: .firstName)
lastName = try container.decode(String.self, forKey: .lastName)
case .post:
title = try container.decode(String.self, forKey: .title)
ownerId = try container.decode(String.self, forKey: .ownerId)
ownerType = try container.decode(String.self, forKey: .ownerType)
case .imageMedium:
assetUrl = try container.decode(String.self, forKey: .assetUrl)
ownerId = try container.decode(String.self, forKey: .ownerId)
ownerType = try container.decode(String.self, forKey: .ownerType)
【讨论】:
That requires to declare all non-global properties as var.
— 真的没有。它需要声明Optional
,但它可以是let
,但你应该在init
内部使用nil
手动初始化它们。【参考方案2】:
这是基于初始编辑,有一些冗余代码,但总体思路应该可以理解:
enum Post: Codable
case post(id: UUID, title: String, ownerId: UUID, ownerType: PostOwner)
case member(id: UUID, firstName: String, lastName: String)
case imageMedium(id: UUID, assetURL: URL, ownerId: UUID, ownerType: ImageOwner)
enum PostType: String, Codable
case post
case member
case imageMedium = "image-medium"
enum PostOwner: String, Codable
case member
enum ImageOwner: String, Codable
case post
enum CodingKeys: String, CodingKey
case id
case type
case title
case assetUrl = "asset-url"
case ownerId = "owner-id"
case ownerType = "owner-type"
case firstName = "first-name"
case lastName = "last-name"
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
let id = try container.decode(UUID.self, forKey: .id)
let type = try container.decode(PostType.self, forKey: .type)
switch type
case .post:
let title = try container.decode(String.self, forKey: .title)
let ownerId = try container.decode(UUID.self, forKey: .ownerId)
let ownerType = try container.decode(PostOwner.self, forKey: .ownerType)
self = .post(id: id, title: title, ownerId: ownerId, ownerType: ownerType)
case .member:
let firstName = try container.decode(String.self, forKey: .firstName)
let lastName = try container.decode(String.self, forKey: .lastName)
self = .member(id: id, firstName: firstName, lastName: lastName)
case .imageMedium:
let assetURL = try container.decode(URL.self, forKey: .assetUrl)
let ownerId = try container.decode(UUID.self, forKey: .ownerId)
let ownerType = try container.decode(ImageOwner.self, forKey: .ownerType)
self = .imageMedium(id: id, assetURL: assetURL, ownerId: ownerId, ownerType: ownerType)
func encode(to encoder: Encoder) throws
var container = encoder.container(keyedBy: CodingKeys.self)
switch self
case .post(let id, let title, let ownerId, let ownerType):
try container.encode(PostType.post, forKey: .type)
try container.encode(id, forKey: .id)
try container.encode(title, forKey: .title)
try container.encode(ownerId, forKey: .ownerId)
try container.encode(ownerType, forKey: .ownerType)
case .member(let id, let firstName, let lastName):
try container.encode(PostType.member, forKey: .type)
try container.encode(id, forKey: .id)
try container.encode(firstName, forKey: .firstName)
try container.encode(lastName, forKey: .lastName)
case .imageMedium(let id, let assetURL, let ownerId, let ownerType):
try container.encode(PostType.imageMedium, forKey: .type)
try container.encode(id, forKey: .id)
try container.encode(assetURL, forKey: .assetUrl)
try container.encode(ownerId, forKey: .ownerId)
try container.encode(ownerType, forKey: .ownerType)
let jsonDecoder = JSONDecoder()
let result = try jsonDecoder.decode([String: [Post]].self, from: yourJSONData)
print(result)
对于当前帖子类型中未使用的字段,它的选项为零,UUID
s 的类型为UUID
,URL
s 的类型为URL
,而不是String
s。
ownerType
被键入为 PostOwner
和 ImageOwner
用于 .post
和 .imageMedium
以获得额外的类型安全性。
编辑:好的,我检查了问题的编辑:在您的 json 中,只有“.post”进入“数据”,其余进入“包含”。在我的答案中,Post
s 和 Included
s 合并为一种类型。
所以应该是这样的:
struct Post: Codable
let id: UUID
let title: String
let ownerId: UUID
let ownerType: PostOwner
enum PostOwner: String, Codable
case member
enum CodingKeys: String, CodingKey
case id
case title
case ownerId = "owner-id"
case ownerType = "owner-type"
enum Included: Codable
case member(id: UUID, firstName: String, lastName: String)
case imageMedium(id: UUID, assetURL: URL, ownerId: UUID, ownerType: ImageOwner)
enum PostType: String, Codable
case member
case imageMedium = "image-medium"
enum ImageOwner: String, Codable
case post
enum CodingKeys: String, CodingKey
case id
case type
case title
case assetUrl = "asset-url"
case ownerId = "owner-id"
case ownerType = "owner-type"
case firstName = "first-name"
case lastName = "last-name"
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
let id = try container.decode(UUID.self, forKey: .id)
let type = try container.decode(PostType.self, forKey: .type)
switch type
case .member:
let firstName = try container.decode(String.self, forKey: .firstName)
let lastName = try container.decode(String.self, forKey: .lastName)
self = .member(id: id, firstName: firstName, lastName: lastName)
case .imageMedium:
let assetURL = try container.decode(URL.self, forKey: .assetUrl)
let ownerId = try container.decode(UUID.self, forKey: .ownerId)
let ownerType = try container.decode(ImageOwner.self, forKey: .ownerType)
self = .imageMedium(id: id, assetURL: assetURL, ownerId: ownerId, ownerType: ownerType)
func encode(to encoder: Encoder) throws
var container = encoder.container(keyedBy: CodingKeys.self)
switch self
case .member(let id, let firstName, let lastName):
try container.encode(PostType.member, forKey: .type)
try container.encode(id, forKey: .id)
try container.encode(firstName, forKey: .firstName)
try container.encode(lastName, forKey: .lastName)
case .imageMedium(let id, let assetURL, let ownerId, let ownerType):
try container.encode(PostType.imageMedium, forKey: .type)
try container.encode(id, forKey: .id)
try container.encode(assetURL, forKey: .assetUrl)
try container.encode(ownerId, forKey: .ownerId)
try container.encode(ownerType, forKey: .ownerType)
Post
类型解析/验证可以/应该通过手动编码init(from: )
来添加。
【讨论】:
【参考方案3】:我想出了如何将混合的included
数组解码为两个数组,每个数组都具有一种类型。与使用一个结构来覆盖多种类型的数据相比,使用两个 Decodable 结构更容易处理,也更通用。
对于任何感兴趣的人来说,这就是我的最终解决方案:
struct Root: Decodable
let data: [Post]?
let members: [Member]
let images: [ImageMedium]
init(from decoder: Decoder) throws
let container = try decoder.container(keyedBy: CodingKeys.self)
data = try container.decode([Post].self, forKey: .data)
var includedArray = try container.nestedUnkeyedContainer(forKey: .included)
var membersArray: [Member] = []
var imagesArray: [ImageMedium] = []
while !includedArray.isAtEnd
do
if let member = try? includedArray.decode(Member.self)
membersArray.append(member)
else if let image = try? includedArray.decode(ImageMedium.self)
imagesArray.append(image)
members = membersArray
images = imagesArray
enum CodingKeys: String, CodingKey
case data
case included
struct Post: Decodable
let id: String?
let type: String?
let title: String?
let ownerId: String?
let ownerType: String?
enum CodingKeys: String, CodingKey
case id
case type
case title
case ownerId = "owner-id"
case ownerType = "owner-type"
struct Member: Decodable
let id: String?
let type: String?
let firstName: String?
let lastName: String?
enum CodingKeys: String, CodingKey
case id
case type
case firstName = "first-name"
case lastName = "last-name"
struct ImageMedium: Decodable
let id: String?
let type: String?
let assetUrl: String?
let ownerId: String?
let ownerType: String?
enum CodingKeys: String, CodingKey
case id
case type
case assetUrl = "asset-url"
case ownerId = "owner-id"
case ownerType = "owner-type"
【讨论】:
该解决方案有效。但它会丢弃从included
解码的元素的排序信息。以上是关于处理包含多种类型的 JSON 数组 - Swift 4 Decodable的主要内容,如果未能解决你的问题,请参考以下文章
使用混合类型和混合键控/非键控在 Swift 中解码 JSON