【问题标题】:Encode/Decode Array of Types conforming to protocol with JSONEncoder使用 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 如果是这样,编译器不应允许协议符合可编码

标签: json swift encoding swift4 codable


【解决方案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 回答的启发。我发现他的方法是合理的,但几乎没有什么可以改进的:

    1. Article 中将数组[Tag][AnyTag] 之间的映射让我们没有自动生成的Codable 一致性
    2. 基类的编码/编码数组不可能有相同的代码,因为static var type 不能在子类中被覆盖。 (例如,如果 TagAuthorTagGenreTag 的超类)
    3. 最重要的是,此代码不能重复用于其他类型,您需要创建新的 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 themselvesTag 不是符合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.
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-10-07
        • 1970-01-01
        • 2021-04-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多