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

前端 未结 3 1917
被撕碎了的回忆
被撕碎了的回忆 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:52

    A simpler version of @CodeDifferent's response, which addresses @JRG-Developer's comment. There is no need to rethink your JSON API; this is a common scenario. For each new ViewLayoutSectionItem you create, you only need to add one case and one line of code to the PartiallyDecodedItem.ItemKind enum and PartiallyDecodedItem.init(from:) method respectively.

    This is not only the least amount of code compared to the accepted answer, it is more performant. In @CodeDifferent's option, you are required to initialize 2 arrays with 2 different representations of the data to get your array of ViewLayoutSectionItems. In this option, you still need to initialize 2 arrays, but get to only have one representation of the data by taking advantage of copy-on-write semantics.

    Also note that it is not necessary to include ItemType in the protocol or the adopting structs (it doesn't make sense to include a string describing what type a type is in a statically typed language).

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

    To handle the snake case -> camel case conversion, rather than manually type out all of the keys, you can simply set a property on JSONDecoder

    struct Sections: Decodable {
        let sections: [ViewLayoutSection]
    }
    
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let sections = try decode(Sections.self, from: json)
        .sections
    

提交回复
热议问题