忽略不支持的Decodables

时间:2018-02-09 09:19:47

标签: json swift enums optional decodable

我一直在我当前的项目中使用Codable非常愉快 - 一切都很好,我开箱即用的大部分内容都是完美的!虽然,最近我偶然发现了第一个真正的问题,它无法按照我想要的方式自动解决。

问题描述
我有一个来自后端的JSON,这是一个嵌套的东西。看起来像这样

{
    "id": "fef08c8d-0b16-11e8-9e00-069b808d0ecc",
    "title": "Challenge_Chapter",
    "topics": [
        {
            "id": "5145ea2c-0b17-11e8-9e00-069b808d0ecc",
            "title": "Automation_Topic",
            "elements": [
                {
                    "id": "518dfb8c-0b18-11e8-9e00-069b808d0ecc",
                    "title": "Automated Line examle",
                    "type": "text_image",
                    "video": null,
                    "challenge": null,
                    "text_image": {
                        "background_url": "",
                        "element_render": ""
                    }
                },
                {
                    "id": "002a1776-0b18-11e8-9e00-069b808d0ecc",
                    "title": "Industry 3.0 vs. 4.0: A vision of the new manufacturing world",
                    "type": "video",
                    "video": {
                        "url": "https://www.youtube.com/watch?v=xxx",
                        "provider": "youtube"
                    },
                    "challenge": null,
                    "text_image": null
                },
                {
                    "id": "272fc2b4-0b18-11e8-9e00-069b808d0ecc",
                    "title": "Classmarker_element",
                    "type": "challenge",
                    "video": null,
                    "challenge": {
                        "url": "https://www.classmarker.com/online-test/start/",
                        "description": null,
                        "provider": "class_marker"
                    },
                    "text_image": null
                }
            ]
        }
    ]
}

是根对象,它包含主题列表,每个主题都包含元素列表。非常简单,但我遇到了最低级别,元素。每个元素都有一个来自后端的枚举,如下所示: [ video, challenge, text_image ],但iOS应用不支持质询,因此我在Swift中的 ElementType 枚举如下所示:

public enum ElementType: String, Codable {
    case textImage = "text_image"
    case video = "video"
}

当然,它是throws,因为发生的第一件事是它试图解码此枚举的challenge值并且它不存在,所以我的整个解码失败。

我想要什么
我只想将解码过程转移到无法解码的忽略 Element。我不需要任何Optional个。我只是希望它们不会出现在Topic的Elements数组中。

我的推理及其缺点
当然,我已经做了几次尝试来解决这个问题。第一个,简单的一个就是将ElementType标记为Optional,但是稍后使用这种方法,我将不得不打开所有内容并处理它 - 这是一项相当繁琐的任务。我的第二个想法是在我的枚举中有类似.unsupported的情况,但是后来我想用它来生成单元格,我将不得不throw或返回Optional - 基本上,和以前的想法一样的问题。 我的最后一个想法,但我还没有测试过,就是为可解码编写一个自定义的init()并以某种方式处理它,但我不确定它是Element还是{{1}应该对此负责?如果我在Topic中写下来,我就不会返回任何内容,我必须Element,但如果我将其放入throw,我将不得不Topic成功将元素解码为数组。如果在某些时候我将直接获取append,那么将会发生这种情况 - 如果没有Elements,我将无法再这样做。

TL; DR
我希望throw不要init(from decoder: Decoder) throws,而是返回throw

2 个答案:

答案 0 :(得分:1)

我建议为所有三种类型创建一个伞形协议

protocol TypeItem {}

编辑:为了符合只考虑两种类型的要求,您必须使用类来获取引用语义

然后创建类TextImageVideo以及采用该协议的Dummy类。解码过程后,Dummy class的所有实例都将被删除。

class TextImage : TypeItem, Decodable {
    let backgroundURL : String
    let elementRender : String

    private enum CodingKeys : String, CodingKey {
        case backgroundURL = "background_url"
        case elementRender = "element_render"
    }
}

class Video : TypeItem, Decodable {
    let url : URL
    let provider : String
}

class Dummy : TypeItem {}

使用枚举正确解码type

enum Type : String, Decodable {
    case text_image, video, challenge
}

在struct Element中,您必须实现一个自定义初始化程序,它根据类型将JSON解码为结构。不需要的challange类型被解码为Dummy实例。由于伞式协议,您只需要一个属性。

class Element : Decodable {
    let type : Type
    let id : String
    let title : String
    let item : TypeItem

    private enum CodingKeys : String, CodingKey {
        case id, title, type, video, text_image
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(String.self, forKey: .id)
        title = try container.decode(String.self, forKey: .title)
        type = try container.decode(Type.self, forKey: .type)
        switch type {
        case .text_image: item = try container.decode(TextImage.self, forKey: .text_image)
        case .video: item = try container.decode(Video.self, forKey: .video)
        default: item = Dummy()
        }
    }
}

最后为根元素创建Root结构,为Topic数组创建topics结构。在Topic中添加一个方法来过滤Dummy个实例。

class Root : Decodable {
    let id : String
    let title : String
    var topics : [Topic]
}

class Topic : Decodable {
    let id : String
    let title : String
    var elements : [Element]

    func filterDummy() {
        elements = elements.filter{!($0.item is Dummy)}
    }
}

在每个filterDummy()中解码后调用Topic以删除死项。 另一个缺点是你必须将item转换为静态类型,例如

let result = try decoder.decode(Root.self, from: data)
result.topics.forEach({$0.filterDummy()})
if let videoElement = result.topics[0].elements.first(where: {$0.type == .video}) {
    let video = videoElement.item as! Video
    print(video.url)
}

答案 1 :(得分:1)

我终于在SR-5953找到了一些相关内容,但我认为这是一个hacky。

无论如何,对于那些允许这种有损解码的好奇者,您需要手动解码所有内容。您可以在init(from decoder: Decoder)中编写它,但更好的方法是编写一个名为struct的新帮助程序FailableCodableArray。实施看起来像:

struct FailableCodableArray<Element: Decodable>: Decodable {
    // https://github.com/phynet/Lossy-array-decode-swift4
    private struct DummyCodable: Codable {}

    private struct FailableDecodable<Base: Decodable>: Decodable {
        let base: Base?

        init(from decoder: Decoder) throws {
            let container = try decoder.singleValueContainer()
            self.base = try? container.decode(Base.self)
        }
    }

    private(set) var elements: [Element]

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        var elements = [Element]()

        if let count = container.count {
            elements.reserveCapacity(count)
        }

        while !container.isAtEnd {
            guard let element = try container.decode(FailableDecodable<Element>.self).base else {
                _ = try? container.decode(DummyCodable.self)
                continue
            }

            elements.append(element)
        }

        self.elements = elements
    }
}

除了对那些可用元素的实际解码之外,你还要编写一个简单的init(from decoder: Decoder)实现,如:

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    elements = try container.decode(FailableCodableArray<Element>.self, forKey: .elements).elements
}

正如我所说,这个解决方案运行良好,但感觉有点 hacky 。这是一个开放错误,所以你可以投票给它,让Swift团队看到,内置的内容将是一个很好的补充!