忽略不支持的解码器

Ignoring not supported Decodables

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


问题描述
我有一个来自后端的 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
                }
            ]
        }
    ]
}

Chapter 是根对象,它包含一个 Topics 列表,每个主题包含一个 Elements 列表。非常简单,但我遇到了最低级别 Elements。每个 Element 都有一个来自后端的枚举,如下所示: [ video, challenge, text_image ],但是 iOS 应用程序不支持挑战,所以我在 Swift 中的 ElementType 枚举看起来像:

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

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

我想要的
我只是希望解码过程 ignore Elements 无法解码。我不需要任何 Optional。我只是希望它们不要出现在 Topic 的元素数组中。

我的推理及其缺点
当然,我已经做了几次尝试来解决这个问题。第一个,也是最简单的一个,就是将 ElementType 标记为 Optional,但稍后使用这种方法时,我将不得不解包所有内容并处理它——这是一项相当乏味的任务。我的第二个想法是在我的枚举中有类似 .unsupported 的情况,但是稍后我想用它来生成单元格,我将不得不 throw 或 return Optional - 基本上,与以前的想法相同的问题。 我的最后一个想法,但我还没有测试过,是为 decodable 编写自定义 init() 并以某种方式在那里处理它,但我不确定它是 Element 还是 Topic 这应该由谁负责?如果我把它写在Element,我不能return什么,我将不得不throw,但如果我把它写在Topic,我将不得不append 成功解码元素到数组。事情是如果在某个时候我将直接获取 Elements 会发生什么 - 再一次,如果没有 throwing 我将无法做到这一点。

TL;DR
我想要 init(from decoder: Decoder) throws 不是 throw,而是 return Optional.

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

protocol TypeItem {}

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

然后创建classes TextImageVideo和一个Dummy class采用协议。 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
}

在结构 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 结构,为 topics 数组创建一个 Topic 结构。在 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{!([=14=].item is Dummy)}
    }
}

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

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

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

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

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。这是一个 open 错误,因此您可以对其投票并让 Swift 团队看到,像这样内置的东西会是一个很好的补充!