Codable 解析 JSON 忽略无效的元素

默认情况下,使用 Swift 内置的 Codable API 编码或解码数组只有全部成功或者全部失败两种情况。可以成功处理所有元素,或者引发错误,这可以说是一个很好的默认设置,因为它可以确保高水平的数据一致性。

但是,有时我们可能希望调整该行为,以便忽略无效元素,而不是导致整个编解码过程失败。例如,假设我们正在使用基于JSON 的 Web API,该API返回当前正在 Swift 中建模的item集合,如下所示:

struct Item: Codable {
    var name: String
    var value: Int
}

extension Item {
    struct Collection: Codable {
        var items: [Item]
    }
}

现在,假设我们正在使用的网络 API 偶尔会返回如下数据,其中包含null 值,而我们的 Swift 代码期望该响应为 Int

{
    "items": [
        {
            "name": "One",
            "value": 1
        },
        {
            "name": "Two",
            "value": 2
        },
        {
            "name": "Three",
            "value": null
        }
    ]
}

如果我们尝试将以上数据解码为Item.Collection模型的实例,那么即使我们的大多数商品确实包含完全有效的数据,整个解码过程也会失败。

上面的示例似乎有些人为设计,但意外遇到格式错误或不一致的JSON 数据其实非常常见,我们可能无法始终调整这些格式以使其完全适应Swift 天然的静态性。

当然,一种潜在的解决方案是简单地将 value 属性设置为可选(Int?),但是这样做可能会在我们的代码库中引入各种复杂性,因为我们现在必须每次都希望拆开这些值。将它们用作具体的,非可选的 Int值。

解决问题的另一种方法是为我们认为可能缺失或无效的属性定义默认值——在我们仍想保留任何包含无效数据的元素的情况下,这是一个很好的解决方案,但是这不是我们今天要讨论的情况。

因此,让我们来看一下如何在解码任何 Decodable 数组时忽略所有无效元素,而不必对 Swift 中数据的结构进行任何的重大修改。

建立有损的可编码列表类型

我们本质上希望做的是将我们的解码过程从非常严格的更改为“有损的”。首先,让我们介绍一个通用的 LossyCodableList 类型,该类型将充当 Element 数组的精简包装:

struct LossyCodableList<Element> {
    var elements: [Element]
}

请注意,我们没有立即使新类型符合 Codable协议,这是因为我们希望它根据要使用的 Element 类型有条件地支持DecodableEncodable 或同时支持这两种类型的协议。毕竟,并非所有类型都可以同时编解码,并且通过分别声明我们对 Codable 协议的支持与否,我们将使新的 LossyCodableList 类型尽可能地灵活。

让我们从 Decodable 开始,我们将遵循中间的 ElementWrapper 类型以可选的方式对每个元素进行解码。然后,我们将使用 compactMap 丢弃所有nil元素,这将为我们提供最终的数组——如下所示:

extension LossyCodableList: Decodable where Element: Decodable {
    private struct ElementWrapper: Decodable {
        var element: Element?

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

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let wrappers = try container.decode([ElementWrapper].self)
        elements = wrappers.compactMap(\.element)
    }
}

接下来,Encodable,它可能不是每个项目都需要的东西,但是在我们还希望为编码过程提供相同的有损行为的情况下,它仍然可以派上用场:

extension LossyCodableList: Encodable where Element: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()

        for element in elements {
            try? container.encode(element)
        }
    }
}

完成上述操作后,我们现在只需将嵌套的Collection类型使用新的LossyCodableList即可自动丢弃所有无效的Item值,如下所示:

extension Item {
    struct Collection: Codable {
        var items: LossyCodableList<Item>
    }
}

使我们的列表类型透明

但是,上述方法的一个主要缺点是,我们现在总是必须使用items.elements 来访问我们的实际项目值,这并不理想。如果可以将LossyCodableList的用法转换为完全透明的实现细节,以使我们可以继续将我们的items属性作为一个简单的值数组进行访问,那将是更好的选择。

一种实现方法是将项目集合的LossyCodableList存储为私有属性,然后在编码或解码时使用CodingKeys类型指向该属性。然后,我们可以将项目实现为计算属性,例如:

extension Item {
    struct Collection: Codable {
        enum CodingKeys: String, CodingKey {
            case _items = "items"
        }

        var items: [Item] {
            get { _items.elements }
            set { _items.elements = newValue } 
        }
        
        private var _items: LossyCodableList<Item>
    }
}

另一个选择是给我们的Collection类型一个完全自定义的Decodable实现,这将涉及在将结果元素分配给我们的items属性之前,使用LossyCodableList解码每个JSON数组:

extension Item {
    struct Collection: Codable {
        enum CodingKeys: String, CodingKey {
            case items
        }

        var items: [Item]

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

以上两种方法都是完美的解决方案,但让我们看看是否可以通过使用Swift的属性包装器功能使事情变得更好。

类型和属性包装器

关于在Swift中实现属性包装器的方式的一件真正整洁的事情是,它们都是标准的Swift类型,这意味着我们可以对LossyCodableList进行改造,使其还可以充当属性包装器。

我们要做的就是用 @propertyWrapper 属性标记它,并实现所需的 wrappedValue 属性(可以再次将其作为计算属性来完成):

@propertyWrapper
struct LossyCodableList<Element> {
    var elements: [Element]

    var wrappedValue: [Element] {
        get { elements }
        set { elements = newValue }
    }
}

完成上述操作后,我们现在可以使用@LossyCodableList属性标记任何基于数组的属性,并且可以对其进行有损编码和解码——相对透明:

extension Item {
    struct Collection: Codable {
        @LossyCodableList var items: [Item]
    }
}

总结

乍一看,Codable 看起来像是一个极其严格且受某种程度限制的API,无论成功还是失败,都没有任何细微差别或自定义的余地。但是,一旦我们超越了表面层次,Codable实际上具有不可思议的强大功能,并且可以通过许多不同的方式进行自定义

静默地忽略无效元素不是永远正确的做法——很多时候,我们确实希望我们的编码过程在遇到任何无效数据时都会失败——但是,如果不是这种情况,那么本文中使用的任何一种技术都可以提供一种很好的方法使我们的编码代码更加灵活和有损,而又不会带来大量额外的复杂性。

译自 John Sundell 的 Ignoring invalid JSON elements when using Codable

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容