How can I implement polymorphic decoding of JSON data in Swift 4?

前端 未结 3 1930
被撕碎了的回忆
被撕碎了的回忆 2021-02-08 19:46

I am attempting to render a view from data returned from an API endpoint. My JSON looks (roughly) like this:

{
  \"sections\": [
    {
      \"title\": \"Feature         


        
3条回答
  •  你的背包
    2021-02-08 19:53

    I recommend you to be judicious on the use of Codable. If you only want to decode a type from JSON and not encode it, conforming it to Decodable alone is enough. And since you have already discovered that you need to decode it manually (via a custom implementation of init(from decoder: Decoder)), the question becomes: what is the least painful way to do it?

    First, the data model. Note that ViewLayoutSectionItemable and its adopters do not conform to 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
    }
    

    Next, here's how we will decode the 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!)
            }
        }
    }
    

    Usage:

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

提交回复
热议问题