我正在尝试使用 Swift 4 中的新 JSONDecoder/Encoder 找到对符合 swift 协议(protocol)的结构数组进行编码/解码的最佳方法。
我编了一个小例子来说明问题:
首先我们有一个协议(protocol)标签和一些符合这个协议(protocol)的类型。
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
}
然后我们有一个类型文章,它有一个标签数组。
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"
}
]
}
问题是在某些时候我必须打开类型属性来解码数组,但要解码数组我必须知道它的类型。
编辑:
我很清楚为什么 Decodable 不能开箱即用,但至少 Encodable 应该可以工作。以下修改后的 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
最佳答案
您的第一个示例无法编译(并且您的第二个示例崩溃)的原因是 protocols don't conform to themselves – Tag
不是符合 Codable
的类型,因此 [Tag]
也不是。因此,Article
不会获得自动生成的 Codable
一致性,因为并非所有属性都符合 Codable
。
只对协议(protocol)中列出的属性进行编码和解码
如果您只想对协议(protocol)中列出的属性进行编码和解码,一种解决方案是简单地使用仅包含这些属性的 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 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
协议(protocol),使其需要符合类型来实现描述其类型的 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
以及基本 Tag
进行编码和解码:
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))
}
}
我们正在使用 super 编码器/解码器,以确保给定符合类型的属性键不会与用于编码该类型的键冲突。例如,编码后的 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)
在上面的代码中。
作为一个可选步骤,我们可以自定义 Article
的 Codable
实现,而不是依赖于自动生成的符合tags
属性属于 [AnyTag]
类型,我们可以提供自己的实现,将 [Tag]
封装到 [ 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]
。
现在我们可以对 TagType
枚举中列出的任何符合 Tag
的类型进行编码和解码:
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")
关于json - 使用 JSONEncoder 编码/解码符合协议(protocol)的类型数组,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44441223/