json - 使用 JSONEncoder 编码/解码符合协议(protocol)的类型数组

标签 json swift encoding swift4 codable

我正在尝试使用 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 themselvesTag 不是符合 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) 在上面的代码中。

作为一个可选步骤,我们可以自定义 ArticleCodable 实现,而不是依赖于自动生成的符合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/

相关文章:

MYSQL 不允许 JSON 数据类型

javascript - 生成与 Leaflet 一起使用的 GeoJSON 网格

c# - 为什么会有 JConstructor?

ios - 警告 : could not load any Objective-C class information in Swift realm

html - 如何在 SwiftUI 文本中显示 HTML 或 Markdown?

linux - 扫描可用的 WiFi 网络会产生奇怪的字符 (Raspbian)

Java escpos 字符集编码

json - 返回列表中名字以某个值开头的对象

swift - 如何在全屏上设置 View 框架(包括导航栏和状态栏)?

Android - å、ä、ö 等字符无法在 WebView 中正确呈现