如何在Swift 4中实现JSON数据的多态解码?

时间:2017-10-05 21:43:58

标签: json swift codable

我正在尝试从API端点返回的数据中呈现视图。我的JSON看起来(大致)是这样的:

{
  "sections": [
    {
      "title": "Featured",
      "section_layout_type": "featured_panels",
      "section_items": [
        {
          "item_type": "foo",
          "id": 3,
          "title": "Bisbee1",
          "audio_url": "http://example.com/foo1.mp3",
          "feature_image_url" : "http://example.com/feature1.jpg"
        },
        {
          "item_type": "bar",
          "id": 4,
          "title": "Mortar8",
          "video_url": "http://example.com/video.mp4",
          "director" : "John Smith",
          "feature_image_url" : "http://example.com/feature2.jpg"
        }
      ]
    }    
  ]
}

我有一个对象,表示如何在我的UI中布局视图。它看起来像这样:

public struct ViewLayoutSection : Codable {
    var title: String = ""
    var sectionLayoutType: String
    var sectionItems: [ViewLayoutSectionItemable] = []
}

ViewLayoutSectionItemable是一种协议,其中包括标题和要在布局中使用的图像的URL。

但是,sectionItems数组实际上由不同类型组成。我想要做的是将每个section项实例化为它自己的类的实例。

如何为init(from decoder: Decoder)设置ViewLayoutSection方法让我迭代该JSON数组中的项目并在每种情况下创建正确类的实例?

5 个答案:

答案 0 :(得分:4)

多态设计是一件好事:许多设计模式都表现出多态性,使整个系统更加灵活和可扩展。

不幸的是,Codable没有"内置"支持多态,至少还没有....还讨论了this is actually a feature or a bug

幸运的是,您可以使用enum作为中间"包装器轻松创建多态对象。"

首先,我建议将itemType声明为static属性,而不是实例属性,以便以后更轻松地启用它。因此,您的协议和多态类型将如下所示:

import Foundation

public protocol ViewLayoutSectionItemable: Decodable {
  static var itemType: String { get }

  var id: Int { get }
  var title: String { get set }
  var imageURL: URL { get set }
}

public struct Foo: ViewLayoutSectionItemable {

  // ViewLayoutSectionItemable Properties
  public static var itemType: String { return "foo" }

  public let id: Int
  public var title: String
  public var imageURL: URL

  // Foo Properties
  public var audioURL: URL
}

public struct Bar: ViewLayoutSectionItemable {

  // ViewLayoutSectionItemable Properties
  public static var itemType: String { return "foo" }

  public let id: Int
  public var title: String
  public var imageURL: URL

  // Bar Properties
  public var director: String
  public var videoURL: URL
}

接下来,为"包装器":

创建一个枚举
public enum ItemableWrapper: Decodable {

  // 1. Keys
  fileprivate enum Keys: String, CodingKey {
    case itemType = "item_type"
    case sections
    case sectionItems = "section_items"
  }

  // 2. Cases
  case foo(Foo)
  case bar(Bar)

  // 3. Computed Properties
  public var item: ViewLayoutSectionItemable {
    switch self {
    case .foo(let item): return item
    case .bar(let item): return item
    }
  }

  // 4. Static Methods
  public static func items(from decoder: Decoder) -> [ViewLayoutSectionItemable] {
    guard let container = try? decoder.container(keyedBy: Keys.self),
      var sectionItems = try? container.nestedUnkeyedContainer(forKey: .sectionItems) else {
        return []
    }
    var items: [ViewLayoutSectionItemable] = []
    while !sectionItems.isAtEnd {
      guard let wrapper = try? sectionItems.decode(ItemableWrapper.self) else { continue }
      items.append(wrapper.item)
    }
    return items
  }

  // 5. Decodable
  public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: Keys.self)
    let itemType = try container.decode(String.self, forKey: Keys.itemType)
    switch itemType {
    case Foo.itemType:  self = .foo(try Foo(from: decoder))
    case Bar.itemType:  self = .bar(try Bar(from: decoder))
    default:
      throw DecodingError.dataCorruptedError(forKey: .itemType,
                                             in: container,
                                             debugDescription: "Unhandled item type: \(itemType)")
    }
  }
}

以上是做什么的:

  1. 您声明与响应结构相关的Keys。在您的API中,您对sectionssectionItems感兴趣。您还需要知道哪个键代表了类型,您在此声明为itemType

  2. 然后您明确列出每个可能的案例:这违反了Open Closed Principle,但这是"好的"这样做是因为它作为一个"工厂"用于创建项目....

    基本上,您只需在整个应用中拥有此 ONCE ,就在这里。

  3. 您为item声明了一个计算属性:这样,您可以打开基础ViewLayoutSectionItemable 而无需需要关心实际的case

  4. 这是"包装"的核心。 factory:您将items(from:)声明为能够返回static的{​​{1}}方法,这正是您想要做的:传入[ViewLayoutSectionItemable]并获取返回一个包含多态类型的数组!这是您实际使用的方法,而不是直接解码DecoderFoo或这些类型的任何其他多态数组。

  5. 最后,您必须Bar实施ItemableWrapper方法。这里的诀窍是Decodable 始终解码ItemWrapper:因此,这符合ItemWrapper期待的方式。

  6. 然而,由于它是Decodable,因此可以使用关联类型,这正是您针对每种情况所做的事情。因此,您可以间接创建多态类型!

    由于您已经在enum内完成了所有繁重的工作,因此 很容易从ItemWrapper转到`[ViewLayoutSectionItemable ],你只是这样做:

    Decoder

答案 1 :(得分:2)

我建议你明智地使用Codable。如果您只想从JSON解码类型而不对其进行编码,那么单独将其与Decodable相符就足够了。既然你已经发现你需要手动解码它(通过init(from decoder: Decoder)的自定义实现),那么问题就变成了:最不痛苦的方法是什么?

首先,数据模型。请注意ViewLayoutSectionItemable及其采用者不符合Decodable

enum ItemType: String, Decodable {
    case foo
    case bar
}

protocol ViewLayoutSectionItemable {
    var id: Int { get }
    var itemType: ItemType { get }
    var title: String { get set }
    var imageURL: URL { get set }
}

struct Foo: ViewLayoutSectionItemable {
    let id: Int
    let itemType: ItemType
    var title: String
    var imageURL: URL
    // Custom properties of Foo
    var audioURL: URL
}

struct Bar: ViewLayoutSectionItemable {
    let id: Int
    let itemType: ItemType
    var title: String
    var imageURL: URL
    // Custom properties of Bar
    var videoURL: URL
    var director: String
}

接下来,我们将如何解码JSON:

struct Sections: Decodable {
    var sections: [ViewLayoutSection]
}

struct ViewLayoutSection: Decodable {
    var title: String = ""
    var sectionLayoutType: String
    var sectionItems: [ViewLayoutSectionItemable] = []

    // This struct use snake_case to match the JSON so we don't have to provide a custom
    // CodingKeys enum. And since it's private, outside code will never see it
    private struct GenericItem: Decodable {
        let id: Int
        let item_type: ItemType
        var title: String
        var feature_image_url: URL
        // Custom properties of all possible types. Note that they are all optionals
        var audio_url: URL?
        var video_url: URL?
        var director: String?
    }

    private enum CodingKeys: String, CodingKey {
        case title
        case sectionLayoutType = "section_layout_type"
        case sectionItems = "section_items"
    }

    public init(from decoder: Decoder) throws {
        let container     = try decoder.container(keyedBy: CodingKeys.self)
        title             = try container.decode(String.self, forKey: .title)
        sectionLayoutType = try container.decode(String.self, forKey: .sectionLayoutType)
        sectionItems      = try container.decode([GenericItem].self, forKey: .sectionItems).map { item in
        switch item.item_type {
        case .foo:
            // It's OK to force unwrap here because we already
            // know what type the item object is
            return Foo(id: item.id, itemType: item.item_type, title: item.title, imageURL: item.feature_image_url, audioURL: item.audio_url!)
        case .bar:
            return Bar(id: item.id, itemType: item.item_type, title: item.title, imageURL: item.feature_image_url, videoURL: item.video_url!, director: item.director!)
        }
    }
}

用法:

let sections = try JSONDecoder().decode(Sections.self, from: json).sections

答案 2 :(得分:0)

@CodeDifferent响应的简单版本,它处理@ JRG-Developer的注释。无需重新考虑您的JSON API;这是常见的情况。对于您创建的每个新的ViewLayoutSectionItem,您只需要分别向PartiallyDecodedItem.ItemKind枚举和PartiallyDecodedItem.init(from:)方法中添加一个大小写和一行代码即可。

与接受的答案相比,这不仅是最少的代码量,而且性能更高。在@CodeDifferent的选项中,您需要使用2种不同的数据表示形式初始化2个数组,以获得ViewLayoutSectionItem的数组。在此选项中,您仍然需要初始化2个数组,但是通过利用写时复制语义只能获得一种数据表示形式。

还请注意,协议或采用的结构中不必包含ItemType(在静态类型化语言中包含描述类型的字符串是没有意义的。)

protocol ViewLayoutSectionItem {
    var id: Int { get }
    var title: String { get }
    var imageURL: URL { get }
}

struct Foo: ViewLayoutSectionItem {
    let id: Int
    let title: String
    let imageURL: URL

    let audioURL: URL
}

struct Bar: ViewLayoutSectionItem {
    let id: Int
    let title: String
    let imageURL: URL

    let videoURL: URL
    let director: String
}

private struct PartiallyDecodedItem: Decodable {
    enum ItemKind: String, Decodable {
        case foo, bar
    }
    let kind: Kind
    let item: ViewLayoutSectionItem

    private enum DecodingKeys: String, CodingKey {
        case kind = "itemType"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: DecodingKeys.self)
        self.kind = try container.decode(Kind.self, forKey: .kind)
        self.item = try {
            switch kind {
            case .foo: return try Foo(from: decoder)
            case .number: return try Bar(from: decoder)
        }()
    }
}

struct ViewLayoutSection: Decodable {
    let title: String
    let sectionLayoutType: String
    let sectionItems: [ViewLayoutSectionItem]

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.title = try container.decode(String.self, forKey: .title)
        self.sectionLayoutType = try container.decode(String.self, forKey: .sectionLayoutType)
        self.sectionItems = try container.decode([PartiallyDecodedItem].self, forKey: .sectionItems)
            .map { $0.item }
    }
}

要处理蛇形案例->驼峰式案例转换,而不是手动键入所有键,只需在JSONDecoder上设置属性即可

struct Sections: Decodable {
    let sections: [ViewLayoutSection]
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let sections = try decode(Sections.self, from: json)
    .sections

答案 3 :(得分:0)

我写了一篇关于这个确切问题的blog post

总之。我建议在 Decoder

上定义一个扩展
extension Decoder {
  func decode<ExpectedType>(_ expectedType: ExpectedType.Type) throws -> ExpectedType {
    let container = try self.container(keyedBy: PolymorphicMetaContainerKeys.self)
    let typeID = try container.decode(String.self, forKey: .itemType)
     
    guard let types = self.userInfo[.polymorphicTypes] as? [Polymorphic.Type] else {
      throw PolymorphicCodableError.missingPolymorphicTypes
    }
     
    let matchingType = types.first { type in
      type.id == typeID
    }
     
    guard let matchingType = matchingType else {
      throw PolymorphicCodableError.unableToFindPolymorphicType(typeID)
    }
     
    let decoded = try matchingType.init(from: self)
     
    guard let decoded = decoded as? ExpectedType else {
      throw PolymorphicCodableError.unableToCast(
        decoded: decoded,
        into: String(describing: ExpectedType.self)
      )
    }
    return decoded
  }
} 

然后将可能的多态类型添加到 Decoder 实例:

var decoder = JSONDecoder()
decoder.userInfo[.polymorphicTypes] = [
  Snake.self,
  Dog.self
]

如果您有嵌套的聚合值,您可以编写一个属性包装器来调用此解码方法,这样您就不需要定义自定义 init(from:)

答案 4 :(得分:0)

这是一个解决这个确切问题的小 utility package

它是围绕配置类型构建的,该配置类型具有可解码类型的变体,定义了类型信息 discriminator

enum DrinkFamily: String, ClassFamily {
    case drink = "drink"
    case beer = "beer"

    static var discriminator: Discriminator = .type
    
    typealias BaseType = Drink

    func getType() -> Drink.Type {
        switch self {
        case .beer:
            return Beer.self
        case .drink:
            return Drink.self
        }
    }
}

稍后在您的集合中重载 init 方法以使用我们的 KeyedDecodingContainer 扩展。

class Bar: Decodable {
    let drinks: [Drink]

    private enum CodingKeys: String, CodingKey {
        case drinks
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        drinks = try container.decodeHeterogeneousArray(OfFamily: DrinkFamily.self, forKey: .drinks)
    }
}