使用 JSONEncoder 编码/解码符合协议的类型数组

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/44441223/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-03 18:55:02  来源:igfitidea点击:

Encode/Decode Array of Types conforming to protocol with JSONEncoder

jsonswiftencodingswift4codable

提问by glektrik

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.

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

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)

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

这是我喜欢的 JSON 结构。

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

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.

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

EDIT:

编辑:

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.

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

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)

And this is the relevant part from Codeable.swift

这是 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.")
}

Source: https://github.com/apple/swift/blob/master/stdlib/public/core/Codable.swift

来源:https: //github.com/apple/swift/blob/master/stdlib/public/core/Codable.swift

回答by Hamish

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

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

Encoding and decoding only the properties listed in the protocol

仅对协议中列出的属性进行编码和解码

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

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

You can then have Articlehold an array of this type-erased wrapper, rather than of Tag:

然后,您可以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)
}

Which outputs the following JSON string:

它输出以下 JSON 字符串:

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

and can be decoded like so:

并且可以像这样解码:

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


Encoding and decoding all properties of the conforming type

编码和解码符合类型的所有属性

If however you need to encode and decoded everyproperty of the given Tagconforming type, you'll likely want to store the type information in the JSON somehow.

但是,如果您需要对给定符合类型的每个属性进行编码和解码Tag,您可能希望以某种方式将类型信息存储在 JSON 中。

I would use an enumin order to do this:

我会使用一个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
        }
    }
}

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.

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

Then you just have to change the Tagprotocol such that it requires conforming types to implement a staticproperty that describes their type:

然后您只需要更改Tag协议,使其需要符合类型来实现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
}

Then we need to adapt the implementation of the type-erased wrapper in order to encode and decode the TagTypealong with the base Tag:

然后我们需要调整类型擦除包装器的实现,以便TagType与 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))
    }
}

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:

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

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

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

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

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

You can pass decoderinstead of container.superDecoder(forKey: .base)& encoderinstead of container.superEncoder(forKey: .base)in the above code.

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

As an optionalstep, we could then customise the Codableimplementation of Articlesuch that rather than relying on an auto-generated conformance with the tagsproperty 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:

作为一个可选的步骤,我们可以自定义这样的Codable实现Article,而不是依赖于自动生成的符合tagstype的属性[AnyTag],我们可以提供我们自己的实现,将 a 装箱[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 { 
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)
}
.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) } }

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

然后,这允许我们将tags属性的类型设为[Tag],而不是[AnyTag]

Now we can encode and decode any Tagconforming type that's listed in our TagTypeenum:

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

{
  "title" : "Article Title",
  "tags" : [
    {
      "type" : "author",
      "base" : {
        "value" : "Author Tag Value",
        "foo" : 56.7
      }
    },
    {
      "type" : "genre",
      "base" : {
        "value" : "Genre Tag Value",
        "baz" : "hello world"
      }
    }
  ]
}

Which outputs the JSON string:

输出 JSON 字符串:

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

and can then be decoded like so:

然后可以像这样解码:

struct MetaArray<M: Meta>: Codable, ExpressibleByArrayLiteral {

    let array: [M.Element]

    init(_ array: [M.Element]) {
        self.array = array
    }

    init(arrayLiteral elements: M.Element...) {
        self.array = elements
    }

    enum CodingKeys: String, CodingKey {
        case metatype
        case object
    }

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()

        var elements: [M.Element] = []
        while !container.isAtEnd {
            let nested = try container.nestedContainer(keyedBy: CodingKeys.self)
            let metatype = try nested.decode(M.self, forKey: .metatype)

            let superDecoder = try nested.superDecoder(forKey: .object)
            let object = try metatype.type.init(from: superDecoder)
            if let element = object as? M.Element {
                elements.append(element)
            }
        }
        array = elements
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try array.forEach { object in
            let metatype = M.metatype(for: object)
            var nested = container.nestedContainer(keyedBy: CodingKeys.self)
            try nested.encode(metatype, forKey: .metatype)
            let superEncoder = nested.superEncoder(forKey: .object)

            let encodable = object as? Encodable
            try encodable?.encode(to: superEncoder)
        }
    }
}

回答by Vadim Pavlov

Inspired by @Hamish answer. I found his approach reasonable, however few things might be improved:

受到@Hamish 回答的启发。我发现他的方法很合理,但几乎没有什么可以改进的地方:

  1. Mapping array [Tag]to and from [AnyTag]in Articleleave us without auto-generated Codableconformance
  2. It's not possible to have same code for coding/encoding array of base class, since static var typecan't be overridden in subclass. (for example if Tagwould be super class of AuthorTag& GenreTag)
  3. Most importantly this code can't be reused for another Type, you required to create new AnyAnotherTypewrapper and it's internal coding/encoding.
  1. 将数组映射[Tag]到和从[AnyTag]inArticle使我们没有自动生成的Codable一致性
  2. 基类的编码/编码数组不可能有相同的代码,因为static var type不能在子类中被覆盖。(例如 ifTagAuthorTag& 的超类GenreTag
  3. 最重要的是,此代码不能重用于其他类型,您需要创建新的 Any AnotherType包装器及其内部编码/编码。

I made slightly different solution, instead of wrapping each element of array, it's possible to make wrapper on entire array:

我做了稍微不同的解决方案,而不是包装数组的每个元素,可以对整个数组进行包装:

protocol Meta: Codable {
    associatedtype Element

    static func metatype(for element: Element) -> Self
    var type: Decodable.Type { get }
}

Where Metais generic protocol:

Meta通用协议在哪里:

enum TagMetatype: String, Meta {

    typealias Element = Tag

    case author
    case genre

    static func metatype(for element: Tag) -> TagMetatype {
        return element.metatype
    }

    var type: Decodable.Type {
        switch self {
        case .author: return AuthorTag.self
        case .genre: return GenreTag.self
        }
    }
}

struct AuthorTag: Tag {
    var metatype: TagMetatype { return .author } // keep computed to prevent auto-encoding
    let value: String
}

struct GenreTag: Tag {
    var metatype: TagMetatype { return .genre } // keep computed to prevent auto-encoding
    let value: String
}

struct Article: Codable {
    let title: String
    let tags: MetaArray<TagMetatype>
}


Now, storing tags will look like:

现在,存储标签将如下所示:

let article = Article(title: "Article Title",
                      tags: [AuthorTag(value: "Author Tag Value"),
                             GenreTag(value:"Genre Tag Value")])

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

Result JSON:

结果 JSON:

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

And in case you want JSON to look even prettier:

如果您希望 JSON 看起来更漂亮:

protocol Meta: Codable {
    associatedtype Element
    static func metatype(for element: Element) -> Self
    var type: Decodable.Type { get }

    init?(rawValue: String)
    var rawValue: String { get }
}

Add to Metaprotocol

添加到Meta协议

struct MetaArray<M: Meta>: Codable, ExpressibleByArrayLiteral {

    let array: [M.Element]

    init(array: [M.Element]) {
        self.array = array
    }

    init(arrayLiteral elements: M.Element...) {
        self.array = elements
    }

    struct ElementKey: CodingKey {
        var stringValue: String
        init?(stringValue: String) {
            self.stringValue = stringValue
        }

        var intValue: Int? { return nil }
        init?(intValue: Int) { return nil }
    }

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()

        var elements: [M.Element] = []
        while !container.isAtEnd {
            let nested = try container.nestedContainer(keyedBy: ElementKey.self)
            guard let key = nested.allKeys.first else { continue }
            let metatype = M(rawValue: key.stringValue)
            let superDecoder = try nested.superDecoder(forKey: key)
            let object = try metatype?.type.init(from: superDecoder)
            if let element = object as? M.Element {
                elements.append(element)
            }
        }
        array = elements
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()
        try array.forEach { object in
            var nested = container.nestedContainer(keyedBy: ElementKey.self)
            let metatype = M.metatype(for: object)
            if let key = ElementKey(stringValue: metatype.rawValue) {
                let superEncoder = nested.superEncoder(forKey: key)
                let encodable = object as? Encodable
                try encodable?.encode(to: superEncoder)
            }
        }
    }
}

And replace CodingKeyswith:

并替换CodingKeys为:

ORIGINAL:
? __lldb_expr_33.Parent
  - title: "Parent Struct"
  ? items: 2 elements
    ? __lldb_expr_33.NumberItem
      - commonProtocolString: "common string from protocol"
      - numberUniqueToThisStruct: 42
    ? __lldb_expr_33.StringItem
      - commonProtocolString: "protocol member string"
      - stringUniqueToThisStruct: "a random string"

ENCODED TO JSON:
{
  "title" : "Parent Struct",
  "items" : [
    {
      "type" : "numberItem",
      "numberUniqueToThisStruct" : 42,
      "commonProtocolString" : "common string from protocol"
    },
    {
      "type" : "stringItem",
      "stringUniqueToThisStruct" : "a random string",
      "commonProtocolString" : "protocol member string"
    }
  ]
}

DECODED FROM JSON:
? __lldb_expr_33.Parent
  - title: "Parent Struct"
  ? items: 2 elements
    ? __lldb_expr_33.NumberItem
      - commonProtocolString: "common string from protocol"
      - numberUniqueToThisStruct: 42
    ? __lldb_expr_33.StringItem
      - commonProtocolString: "protocol member string"
      - stringUniqueToThisStruct: "a random string"

回答by pkamb

Drawn from the accepted answer, I ended up with the following code that can be pasted into an Xcode Playground. I used this base to add a codable protocol to my app.

从接受的答案中得出,我最终得到了以下可以粘贴到 Xcode Playground 中的代码。我使用这个基础向我的应用程序添加了一个可编码的协议。

The output looks like this, withoutthe nesting mentioned in the accepted answer.

输出看起来像这样,没有在接受的答案中提到的嵌套。

import Foundation

struct Parent: Codable {
    let title: String
    let items: [Item]

    init(title: String, items: [Item]) {
        self.title = title
        self.items = items
    }

    enum CodingKeys: String, CodingKey {
        case title
        case items
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encode(title, forKey: .title)
        try container.encode(items.map({ AnyItem(
struct Tag: Codable {
  let type: TagType
  let value: String

  enum TagType: String, Codable {
    case author
    case genre
  }
}
) }), forKey: .items) } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) title = try container.decode(String.self, forKey: .title) items = try container.decode([AnyItem].self, forKey: .items).map {
struct Tag: Codable {
    let type: TagType
    let value: String

    enum TagType: String, Codable {
        case author
        case genre
    }
}

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

    enum CodingKeys: String, CodingKey {
        case tags
        case title
    }
}

.item } } } protocol Item: Codable { static var type: ItemType { get } var commonProtocolString: String { get } } enum ItemType: String, Codable { case numberItem case stringItem var metatype: Item.Type { switch self { case .numberItem: return NumberItem.self case .stringItem: return StringItem.self } } } struct NumberItem: Item { static var type = ItemType.numberItem let commonProtocolString = "common string from protocol" let numberUniqueToThisStruct = 42 } struct StringItem: Item { static var type = ItemType.stringItem let commonProtocolString = "protocol member string" let stringUniqueToThisStruct = "a random string" } struct AnyItem: Codable { var item: Item init(_ item: Item) { self.item = item } private enum CodingKeys : CodingKey { case type case item } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(type(of: item).type, forKey: .type) try item.encode(to: encoder) } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(ItemType.self, forKey: .type) self.item = try type.metatype.init(from: decoder) } } func testCodableProtocol() { var items = [Item]() items.append(NumberItem()) items.append(StringItem()) let parent = Parent(title: "Parent Struct", items: items) print("ORIGINAL:") dump(parent) print("") let jsonEncoder = JSONEncoder() jsonEncoder.outputFormatting = .prettyPrinted let jsonData = try! jsonEncoder.encode(parent) let jsonString = String(data: jsonData, encoding: .utf8)! print("ENCODED TO JSON:") print(jsonString) print("") let jsonDecoder = JSONDecoder() let decoded = try! jsonDecoder.decode(type(of: parent), from: jsonData) print("DECODED FROM JSON:") dump(decoded) print("") } testCodableProtocol()

Paste into your Xcode project or Playground and customize to your liking:

粘贴到您的 Xcode 项目或 Playground 中并根据您的喜好进行自定义:

##代码##

回答by WizMeister

Why wouldn't you use enums for the type of the tag?

为什么不使用枚举作为标签类型?

##代码##

Then you can encode like try? JSONEncoder().encode(tag)or decode like let tags = try? JSONDecoder().decode([Tag].self, from: jsonData)and do any sort of processing as filtering the tags by type. You can do the same for the Article struct as well:

然后,您可以对 like 进行编码try? JSONEncoder().encode(tag)或解码,let tags = try? JSONDecoder().decode([Tag].self, from: jsonData)并进行任何类型的处理,如按类型过滤标签。您也可以对 Article 结构执行相同的操作:

##代码##