使用JSONEncoder对符合协议的类型进行编码/解码

时间:2017-06-08 16:34:44

标签: json swift encoding swift4 codable

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

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

首先,我们有一个协议标签和一些符合此协议的类型。

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,它有一个标签数组。

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

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

修改

我很清楚为什么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)

这是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

5 个答案:

答案 0 :(得分:67)

您的第一个示例未编译(以及您的第二次崩溃)的原因是protocols don't conform to themselves - Tag不是符合Codable的类型,因此[Tag]也不符合Article 1}}。因此,Codable不会获得自动生成的Codable一致性,因为并非所有属性都符合AnyTag

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

如果您只想对协议中列出的属性进行编码和解码,一种解决方案就是简单地使用只保存这些属性的Codable类型橡皮擦,然后可以提供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) }

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

哪个输出以下JSON字符串:

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

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

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

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

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

然后您只需要更改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)) } } 进行编码和解码:

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

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

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

但是,如果您知道不会发生冲突,并希望在相同级别对属性进行编码/解码,那么JSON看起来像这样:

decoder

您可以通过container.superDecoder(forKey: .base)代替encoder&amp; <{1}}代替上述代码中的container.superEncoder(forKey: .base)

作为可选步骤,我们可以自定义Codable的{​​{1}}实施,而不是依赖于与Article的自动生成的一致性属性为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]枚举中列出的任何Tag符合类型进行编码和解码:

TagType

哪个输出JSON字符串:

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

然后可以这样解码:

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

答案 1 :(得分:3)

灵感来自@Hamish答案。我发现他的方法合理,但是可能有几处改进:

  1. .*$中与[Tag]之间来回映射数组[AnyTag]使我们没有自动生成的Article一致性
  2. 对于基类的编码/编码数组,不可能具有相同的代码,因为Codable不能在子类中被覆盖。 (例如,如果static var typeTagAuthorTag的超类)
  3. 最重要的是,该代码无法重用于其他类型,您需要创建新的Any AnotherType 包装器及其内部编码/编码。

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

GenreTag

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) } } } 是通用协议:

Meta

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

protocol Meta: Codable {
    associatedtype Element

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

结果JSON:

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

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

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

添加到{ "title" : "Article Title", "tags" : [ { "author" : { "value" : "Author Tag Value" } }, { "genre" : { "value" : "Genre Tag Value" } } ] } 协议

Meta

并将protocol Meta: Codable { associatedtype Element static func metatype(for element: Element) -> Self var type: Decodable.Type { get } init?(rawValue: String) var rawValue: String { get } } 替换为:

CodingKeys

答案 2 :(得分:2)

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

输出如下所示,没有接受的答案中提到的嵌套。

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"

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

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($0) }), 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 { $0.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()

答案 3 :(得分:1)

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

struct Tag: Codable {
  let type: TagType
  let value: String

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

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

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

答案 4 :(得分:-3)

这是如何为Swift 4编码/解码struct数组的示例。非常感谢Alex Gibson

import UIKit

struct Person: Codable {
  var name:String
}

class TestEncodeDecode: NSObject {

  func run() {

    // create
    let person1:Person = Person(name: "Joe")
    let person2:Person = Person(name: "Jay")
    let persons:[Person] = [person1, person2]

    // save
    let encoder = JSONEncoder()
    if let encoded = try? encoder.encode(persons) {
        UserDefaults.standard.set(encoded, forKey: "persons")
    }

    // load
    if let personsData = UserDefaults.standard.value(forKey: "persons") as? Data {
        let decoder = JSONDecoder()
        if let loadPersons = try? decoder.decode(Array.self, from: personsData) as [Person]{
            loadPersons.forEach { print($0) }
        }
    }
  }
}

输出:

Person(name: "Joe")
Person(name: "Jay")