我正在尝试找到使用Swift 4中新的JSONDecoder / Encoder对符合swift协议的结构数组进行编码/解码的最佳方法。
我做了一个小例子来说明这个问题:
首先,我们有一个协议标签和一些符合该协议的类型。
protocol Tag: Codable { var type: String { get } var value: String { get } } struct AuthorTag: Tag { let type = "author" let value: String } struct GenreTag: Tag { let type = "genre" let value: String }
然后我们有一个带有标签数组的Type Article。
struct Article: Codable { let tags: [Tag] let title: String }
最后,我们对文章进行编码或解码
let article = Article(tags: [AuthorTag(value: "Author Tag Value"), GenreTag(value:"Genre Tag Value")], title: "Article Title") let jsonEncoder = JSONEncoder() let jsonData = try jsonEncoder.encode(article) let jsonString = String(data: jsonData, encoding: .utf8)
这是我喜欢的JSON结构。
{ "title": "Article Title", "tags": [ { "type": "author", "value": "Author Tag Value" }, { "type": "genre", "value": "Genre Tag Value" } ] }
问题是,在某些时候,我必须打开type属性以解码Array,但是要解码Array,我必须知道其类型。
编辑:
对我来说很清楚,为什么“可分解的东西”不能开箱即用,但至少“可编码的”应该可以工作。下面的修改后的Article结构可以编译,但由于以下错误消息而崩溃。
fatal error: Array<Tag> does not conform to Encodable because Tag does not conform to Encodable.: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-900.0.43/src/swift/stdlib/public/core/Codable.swift, line 3280 struct Article: Encodable { let tags: [Tag] let title: String enum CodingKeys: String, CodingKey { case tags case title } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(tags, forKey: .tags) try container.encode(title, forKey: .title) } } let article = Article(tags: [AuthorTag(value: "Author Tag"), GenreTag(value:"A Genre Tag")], title: "A Title") let jsonEncoder = JSONEncoder() let jsonData = try jsonEncoder.encode(article) let jsonString = String(data: jsonData, encoding: .utf8)
这是Codeable.swift的相关部分
guard Element.self is Encodable.Type else { preconditionFailure("\(type(of: self)) does not conform to Encodable because \(Element.self) does not conform to Encodable.") }
来源:https : //github.com/apple/swift/blob/master/stdlib/public/core/Codable.swift
您的第一个示例无法编译(第二次崩溃)的原因是因为协议不符合自身 – 不是符合Tag的类型Codable,因此也不符合[Tag]。因此Article不会获得自动生成的Codable一致性,因为并非其所有属性都符合Codable。
Tag
Codable
[Tag]
Article
如果只想对协议中列出的属性进行编码和解码,则一种解决方案是仅使用AnyTag仅保留这些属性的类型擦除器,然后提供Codable一致性。
AnyTag
然后,您可以拥有Article此类型擦除包装器的数组,而不是Tag:
struct AnyTag : Tag, Codable { let type: String let value: String init(_ base: Tag) { self.type = base.type self.value = base.value } } struct Article: Codable { let tags: [AnyTag] let title: String } let tags: [Tag] = [ AuthorTag(value: "Author Tag Value"), GenreTag(value:"Genre Tag Value") ] let article = Article(tags: tags.map(AnyTag.init), title: "Article Title") let jsonEncoder = JSONEncoder() jsonEncoder.outputFormatting = .prettyPrinted let jsonData = try jsonEncoder.encode(article) if let jsonString = String(data: jsonData, encoding: .utf8) { print(jsonString) }
输出以下JSON字符串:
{ "title" : "Article Title", "tags" : [ { "type" : "author", "value" : "Author Tag Value" }, { "type" : "genre", "value" : "Genre Tag Value" } ] }
可以这样解码:
let decoded = try JSONDecoder().decode(Article.self, from: jsonData) print(decoded) // Article(tags: [ // AnyTag(type: "author", value: "Author Tag Value"), // AnyTag(type: "genre", value: "Genre Tag Value") // ], title: "Article Title")
但是,如果您需要对给定符合类型的 每个 属性进行编码和解码,则Tag可能需要以某种方式将类型信息存储在JSON中。
我将使用enum来执行此操作:
enum
enum TagType : String, Codable { // be careful not to rename these – the encoding/decoding relies on the string // values of the cases. If you want the decoding to be reliant on case // position rather than name, then you can change to enum TagType : Int. // (the advantage of the String rawValue is that the JSON is more readable) case author, genre var metatype: Tag.Type { switch self { case .author: return AuthorTag.self case .genre: return GenreTag.self } } }
这比仅使用普通字符串表示类型更好,因为编译器可以检查我们是否为每种情况提供了元类型。
然后,您只需要更改Tag协议,使其需要符合标准的类型即可实现static描述其类型的属性:
static
protocol Tag : Codable { static var type: TagType { get } var value: String { get } } struct AuthorTag : Tag { static var type = TagType.author let value: String var foo: Float } struct GenreTag : Tag { static var type = TagType.genre let value: String var baz: String }
然后,我们需要调整类型擦除包装器的实现,以便TagType与base一起编码和解码Tag:
TagType
struct AnyTag : Codable { var base: Tag init(_ base: Tag) { self.base = base } private enum CodingKeys : CodingKey { case type, base } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(TagType.self, forKey: .type) self.base = try type.metatype.init(from: container.superDecoder(forKey: .base)) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(type(of: base).type, forKey: .type) try base.encode(to: container.superEncoder(forKey: .base)) } }
我们使用超级编码器/解码器,以确保给定符合类型的属性键不会与用于编码该类型的键冲突。例如,编码的JSON将如下所示:
{ "type" : "author", "base" : { "value" : "Author Tag Value", "foo" : 56.7 } }
但是,如果您知道不会有冲突,并且希望在与“类型”键 相同的 级别上对属性进行编码/解码,则JSON如下所示:
{ "type" : "author", "value" : "Author Tag Value", "foo" : 56.7 }
您可以通过传递decoder而不是container.superDecoder(forKey: .base)&encoder代替container.superEncoder(forKey: .base)上面的代码。
decoder
container.superDecoder(forKey: .base)
encoder
container.superEncoder(forKey: .base)
作为 可选 步骤,我们然后可以自定义Codable实现Article,而不是依赖于tags类型为type 的属性的自动生成的符合性[AnyTag],我们可以提供自己的实现,将a打包[Tag]成一个[AnyTag]before编码,然后将unbox解码:
tags
[AnyTag]
struct Article { let tags: [Tag] let title: String init(tags: [Tag], title: String) { self.tags = tags self.title = title } } extension Article : Codable { private enum CodingKeys : CodingKey { case tags, title } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.tags = try container.decode([AnyTag].self, forKey: .tags).map { $0.base } self.title = try container.decode(String.self, forKey: .title) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(tags.map(AnyTag.init), forKey: .tags) try container.encode(title, forKey: .title) } }
然后,这使我们可以将tags属性的类型设为[Tag],而不是[AnyTag]。
现在我们可以对枚举中Tag列出的任何符合类型进行编码和解码TagType:
let tags: [Tag] = [ AuthorTag(value: "Author Tag Value", foo: 56.7), GenreTag(value:"Genre Tag Value", baz: "hello world") ] let article = Article(tags: tags, title: "Article Title") let jsonEncoder = JSONEncoder() jsonEncoder.outputFormatting = .prettyPrinted let jsonData = try jsonEncoder.encode(article) if let jsonString = String(data: jsonData, encoding: .utf8) { print(jsonString) }
输出JSON字符串:
{ "title" : "Article Title", "tags" : [ { "type" : "author", "base" : { "value" : "Author Tag Value", "foo" : 56.7 } }, { "type" : "genre", "base" : { "value" : "Genre Tag Value", "baz" : "hello world" } } ] }
然后可以像这样解码:
let decoded = try JSONDecoder().decode(Article.self, from: jsonData) print(decoded) // Article(tags: [ // AuthorTag(value: "Author Tag Value", foo: 56.7000008), // GenreTag(value: "Genre Tag Value", baz: "hello world") // ], // title: "Article Title")