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

查看:102
本文介绍了使用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
}

然后我们有一个带有标签数组的Type Article.

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.

对我来说很清楚,为什么可分解的东西"不能开箱即用,但至少可编码的"应该起作用.下面的修改后的Article结构可以编译,但会崩溃,并显示以下错误消息.

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 /apple/swift/blob/master/stdlib/public/core/Codable.swift

推荐答案

第一个示例未编译(第二个崩溃)的原因是

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类型擦除器,然后提供Codable一致性.

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属性的类型为,我们可以提供我们自己的实现,在编码之前将[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天全站免登陆