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

Jef*_*eff 9 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"
        }
      ]
    }    
  ]
}
Run Code Online (Sandbox Code Playgroud)

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

public struct ViewLayoutSection : Codable {
    var title: String = ""
    var sectionLayoutType: String
    var sectionItems: [ViewLayoutSectionItemable] = []
}
Run Code Online (Sandbox Code Playgroud)

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

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

如何设置的init(from decoder: Decoder)方法,ViewLayoutSection以便让我遍历该JSON数组中的项目并在每种情况下创建适当的类的实例?

JRG*_*per 7

Polymorphic design is a good thing: many design patterns exhibit polymorphism to make the overall system more flexible and extensible.

Unfortunately, Codable doesn't have "built in" support for polymorphism, at least not yet.... there's also discussion about whether this is actually a feature or a bug.

Fortunately, you can pretty easily create polymorphic objects using an enum as an intermediate "wrapper."

First, I'd recommend declaring itemType as a static property, instead of an instance property, to make switching on it easier later. Thereby, your protocol and polymorphic types would look like this:

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

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

  // Bar Properties
  public var director: String
  public var videoURL: URL
}
Run Code Online (Sandbox Code Playgroud)

Next, create an enum for the "wrapper":

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)")
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

Here's what the above does:

  1. You declare Keys that are relevant to the response's structure. In your given API, you're interested in sections and sectionItems. You also need to know which key represents the type, which you declare here as itemType.

  2. You then explicitly list every possible case: this violates the Open Closed Principle, but this is "okay" to do as it's acting as a "factory" for creating items....

    Essentially, you'll only have this ONCE throughout your entire app, just right here.

  3. You declare a computed property for item: this way, you can unwrap the underlying ViewLayoutSectionItemable without needing to care about the actual case.

  4. This is the heart of the "wrapper" factory: you declare items(from:) as a static method that's capable of returning [ViewLayoutSectionItemable], which is exactly what you want to do: pass in a Decoder and get back an array containing polymorphic types! This is the method you'll actually use instead of decoding Foo, Bar or any other polymorphic arrays of these types directly.

  5. Lastly, you must make ItemableWrapper implement the Decodable method. The trick here is that ItemWrapper always decodes an ItemWrapper: thereby, this works how Decodable is expecting.

As it's an enum, however, it's allowed to have associated types, which is exactly what you do for each case. Hence, you can indirectly create polymorphic types!

由于您已经完成了所有繁重的工作ItemWrapper,因此现在很容易从a Decoder转到`[ViewLayoutSectionItemable],您可以像这样简单地进行操作:

let decoder = ... // however you created it
let items = ItemableWrapper.items(from: decoder)
Run Code Online (Sandbox Code Playgroud)


jja*_*tie 5

@CodeDifferent 响应的更简单版本,用于解决 @JRG-Developer 的评论。无需重新考虑您的 JSON API;这是一个常见的场景。对于ViewLayoutSectionItem您创建的每个 new ,您只需要分别在PartiallyDecodedItem.ItemKindenum 和PartiallyDecodedItem.init(from:)method 中添加一个 case 和一行代码。

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

还要注意,没有必要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 }
    }
}
Run Code Online (Sandbox Code Playgroud)

要处理蛇形大小写 -> 驼色大小写转换,而不是手动输入所有键,您可以简单地设置一个属性 JSONDecoder

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

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let sections = try decode(Sections.self, from: json)
    .sections
Run Code Online (Sandbox Code Playgroud)


Cod*_*ent 1

我建议您谨慎使用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
}
Run Code Online (Sandbox Code Playgroud)

接下来,我们将如何解码 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!)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

用法:

let sections = try JSONDecoder().decode(Sections.self, from: json).sections
Run Code Online (Sandbox Code Playgroud)

  • 除非您的类型确实_仅_限于“Foo”和“Bar”(根据我的经验,这不太可能永远保持不变),否则这种设计很快就会失控。问题是这个注释:“所有可能类型的自定义属性”...如果稍后需要添加_许多_新类型怎么办?或者每种类型都可能有许多不同的属性?`GenericItem` 很快就会变得巨大......!这不好......只要有可能,努力将逻辑转移到多态对象并保持工厂/中间对象较小。 (2认同)