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

Posted

技术标签:

【中文标题】使用 JSONEncoder 编码/解码符合协议的类型数组【英文标题】:Encode/Decode Array of Types conforming to protocol with JSONEncoder 【发布时间】:2017-11-10 11:50:45 【问题描述】:

我正在尝试使用 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

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

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 应该可以工作。以下修改后的 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

【问题讨论】:

为什么要将 AuthorTagGenreTag 分开类型?它们都具有完全相同的界面,而且您似乎只是在使用type 属性来区分它们(尽管实际上应该是enum)。 这只是一个简化的例子。他们可以有单独的属性。我也想过将 type 设为 enum,但如果 type 是 enum,我无法在不修改 enum 的情况下添加新类型。 代码是否实际工作并生成您包含的 JSON?我得到了Type 'Article' does not conform to protocol 'Decodable''Encodable' @ThatlazyiosGuy웃 我看不出这是个错误——Tag 不符合 Codable(因此也不符合 [Tag]),因为 protocols don't conform to themselves。考虑一下Tag 是否符合Codable——解码器尝试解码为任意Tag 时会发生什么?应该创建什么具体类型? @Hamish 如果是这样,编译器不应允许协议符合可编码 【参考方案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
    


【讨论】:

【参考方案2】:

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

    Article 中将数组[Tag][AnyTag] 之间的映射让我们没有自动生成的Codable 一致性 基类的编码/编码数组不可能有相同的代码,因为static var type 不能在子类中被覆盖。 (例如,如果 TagAuthorTagGenreTag 的超类) 最重要的是,此代码不能重复用于其他类型,您需要创建新的 AnyAnotherType 包装器,并且它是内部编码/编码。

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

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 


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

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

如果你想让 JSON 看起来更漂亮:


  "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 替换为:

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)
            
        
    

【讨论】:

【参考方案3】:

从接受的答案中得出,我最终得到了可以粘贴到 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()

【讨论】:

【参考方案4】:

您的第一个示例无法编译(并且您的第二个示例崩溃)的原因是因为protocols don't conform to themselves – Tag 不是符合Codable 的类型,因此[Tag] 也不是。因此Article 不会自动生成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)

输出以下 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 协议,使其需要符合类型来实现描述其类型的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))
    

我们正在使用超级编码器/解码器,以确保给定符合类型的属性键不会与用于编码该类型的键冲突。例如,编码后的 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 类型为@987654355 的属性的一致性@,我们可以提供我们自己的实现,在编码之前将[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 枚举中列出的任何符合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")

【讨论】:

哇。请让我说这是一个很好的答案,不要在讨论中添加任何内容! 我试过了,它非常适合创建对象,但我遇到了对象的自定义属性困扰解码器的问题。我总是收到“无法获取键控解码容器 - 而是找到空值”。你知道这里有什么帮助吗?示例:AuthorTag 中的“foo” var,只要添加此行,就会出现错误。 @palme 啊,您可能正在使用期望基值编码为单独对象(在“基”键下)的解码逻辑。如果您希望基值的属性与“类型”键处于同一级别,您希望在解码/编码中传递 decoder 而不是 container.superDecoder(forKey: .base)encoder 而不是 container.superEncoder(forKey: .base) AnyTag的逻辑。 再次感谢您快速准确的回答!它奏效了。 @kunwang 恐怕你需要做一些相当参与的type erasure。这是一个粗略的例子:gist.github.com/hamishknight/5ffe87a43590a1f1fae8e341cf0da418.

以上是关于使用 JSONEncoder 编码/解码符合协议的类型数组的主要内容,如果未能解决你的问题,请参考以下文章

text JSONEncoder对数据进行编码/解码

使用 JSONEncoder 对类型为 Codable 的变量进行编码

使用 Swift 的 Encodable 将可选属性编码为 null 而无需自定义编码

如果某些东西符合 Codable ,它会永远无法被编码或解码吗?

JSONDecoder和JSONEncoder类是线程安全的吗?

使用 JSONEncoder 将 nil 值编码为 null