使用 JSONEncoder 编码/解码符合协议的类型数组 [英] Encode/Decode Array of Types conforming to protocol with JSONEncoder

查看:39
本文介绍了使用 JSONEncoder 编码/解码符合协议的类型数组的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试使用 Swift 4 中的新 JSONDecoder/Encoder 找到对符合 swift 协议的结构数组进行编码/解码的最佳方法.

I'm trying to find the best way to Encode/Decode an array of structs conforming to a swift protocol using the new JSONDecoder/Encoder in Swift 4.

我编了一个小例子来说明问题:

I made up a little example to illustrate the problem:

首先我们有一个协议标签和一些符合这个协议的类型.

First we have a protocol Tag and some Types that conform to this 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
}

然后我们有一个类型文章,它有一个标签数组.

Then we have a Type Article which has an Array of Tags.

struct Article: Codable {
    let tags: [Tag]
    let title: String
}

最后我们对文章进行编码或解码

Finally we encode or decode the Article

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 结构.

And this is the JSON structure that I like to have.

{
 "title": "Article Title",
 "tags": [
     {
       "type": "author",
       "value": "Author Tag Value"
     },
     {
       "type": "genre",
       "value": "Genre Tag Value"
     }
 ]
}

问题是在某些时候我必须打开 type 属性来解码数组,但要解码数组我必须知道它的类型.

The problem is that at some point I have to switch on the type property to decode the Array but to Decode the Array I have to know its type.

我很清楚为什么 Decodable 不能开箱即用,但至少 Encodable 应该可以工作.以下修改后的文章结构编译但崩溃并显示以下错误消息.

It's clear to me why Decodable can not work out of the box but at least Encodable should work. The following modified Article struct compiles but crashes with the following error message.

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 的相关部分

And this is the relevant part from 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/苹果/swift/blob/master/stdlib/public/core/Codable.swift

推荐答案

你的第一个例子没有编译(第二次崩溃)的原因是因为 协议不符合自身Tag 不是符合 Codable 的类型,因此 [Tag] 也不是.因此 Article 不会获得自动生成的 Codable 一致性,因为并非其所有属性都符合 Codable.

The reason why your first example doesn't compile (and your second crashes) is because protocols don't conform to themselves – Tag is not a type that conforms to Codable, therefore neither is [Tag]. Therefore Article doesn't get an auto-generated Codable conformance, as not all of its properties conform to Codable.

如果您只想对协议中列出的属性进行编码和解码,一种解决方案是简单地使用仅包含这些属性的 AnyTag 类型橡皮擦,然后可以提供 可编码一致性.

If you just want to encode and decode the properties listed in the protocol, one solution would be to simply use an AnyTag type-eraser that just holds those properties, and can then provide the Codable conformance.

然后你可以让 Article 保存一个这个类型擦除包装器的数组,而不是 Tag:

You can then have Article hold an array of this type-erased wrapper, rather than of 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 字符串:

Which outputs the following JSON string:

{
  "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 中.


Encoding and decoding all properties of the conforming type

If however you need to encode and decoded every property of the given Tag conforming type, you'll likely want to store the type information in the JSON somehow.

我会使用 enum 来做到这一点:

I would use an enum in order to do this:

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
        }
    }
}

这比仅使用普通字符串来表示类型要好,因为编译器可以检查我们是否为每种情况提供了元类型.

Which is better than just using plain strings to represent the types, as the compiler can check that we've provided a metatype for each case.

然后你只需要改变 Tag 协议,这样它就需要符合类型来实现一个描述它们的类型的 static 属性:

Then you just have to change the Tag protocol such that it requires conforming types to implement a static property that describes their type:

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 进行编码和解码:

Then we need to adapt the implementation of the type-erased wrapper in order to encode and decode the TagType along with the base 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))
    }
}

我们正在使用超级编码器/解码器,以确保给定符合类型的属性键不会与用于编码类型的键发生冲突.例如,编码后的 JSON 将如下所示:

We're using a super encoder/decoder in order to ensure that the property keys for the given conforming type don't conflict with the key used to encode the type. For example, the encoded JSON will look like this:

{
  "type" : "author",
  "base" : {
    "value" : "Author Tag Value",
    "foo" : 56.7
  }
}

但是,如果您知道不会有冲突,并且希望在与类型"键相同的级别对属性进行编码/解码,则 JSON 如下所示:

If however you know there won't be a conflict, and want the properties to be encoded/decoded at the same level as the "type" key, such that the JSON looks like this:

{
  "type" : "author",
  "value" : "Author Tag Value",
  "foo" : 56.7
}

您可以传递 decoder 而不是 container.superDecoder(forKey: .base) &encoder 而不是上面代码中的 container.superEncoder(forKey: .base).

You can pass decoder instead of container.superDecoder(forKey: .base) & encoder instead of container.superEncoder(forKey: .base) in the above code.

作为一个可选步骤,我们可以自定义ArticleCodable实现,而不是依赖于自动生成的符合tags 属性属于 [AnyTag] 类型,我们可以提供自己的实现,将 [Tag] 装箱到 [AnyTag] 编码前,然后拆箱解码:

As an optional step, we could then customise the Codable implementation of Article such that rather than relying on an auto-generated conformance with the tags property being of type [AnyTag], we can provide our own implementation that boxes up a [Tag] into an [AnyTag] before encoding, and then unbox for decoding:

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].

This then allows us to have the tags property be of type [Tag], rather than [AnyTag].

现在我们可以编码和解码 TagType 枚举中列出的任何符合 Tag 的类型:

Now we can encode and decode any Tag conforming type that's listed in our TagType enum:

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 字符串:

Which outputs the JSON string:

{
  "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")

这篇关于使用 JSONEncoder 编码/解码符合协议的类型数组的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆