专栏首页韦弦的偶尔分享Codable 解析 JSON 忽略无效的元素

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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Codable 自定义解析 JSON

    大多数现代应用程序的共同点是,它们需要对各种形式的数据进行编码或解码。无论是通过网络下载的JSON数据,还是存储在本地的模型的某种形式的序列化表示形式,对于几乎...

    韦弦zhy
  • Encoding and Decoding Custom Types

    许多编程任务涉及通过网络连接发送数据,将数据保存到磁盘或将数据提交到API和服务。 这些任务通常要求在传输数据时将数据编码和解码为中间格式。

    SheltonWan
  • Hacking with iOS: SwiftUI Edition - 里程碑:项目 10 - 12

    这最后三个项目确实推动了数据的开发,首先是通过互联网发送和接收数据,然后进入Core Data,以便您可以了解实际应用如何管理其数据。您在此项目中学到的技能也许...

    韦弦zhy
  • Swift 项目中涉及到 JSONDecoder,网络请求,泛型协议式编程的一些记录和想法

    最近项目开发一直在使用 swift,因为 HTN 项目最近会有另外一位同事加入,所以打算对最近涉及到的一些技术和自己的一些想法做个记录,同时也能够方便同事熟悉代...

    用户7451029
  • Hacking with iOS: SwiftUI Edition - Moonshot 项目(一)

    在这个项目中,我们将构建一个应用程序,让用户了解组成美国宇航局阿波罗太空计划的任务和宇航员。您将获得更多的使用Codable的经验,但更重要的是,您还将使用滚动...

    韦弦zhy
  • 开源 , KoobooJson一款高性能且轻量的JSON框架

      在C#领域,有很多成熟的开源JSON框架,其中最著名且使用最多的是 Newtonsoft.Json ,然而因为版本迭代,其代码要兼容从net2.0到现在的最...

    小曾看世界
  • Hacking with iOS: SwiftUI Edition - 纸杯蛋糕项目(二)

    我们对代码进行了组织,以使我们在所有界面之间共享一个Order对象,其优点是我们可以在这些界面之间来回移动而不会丢失数据。但是,这种方法需要付出一定的代价:我们...

    韦弦zhy
  • 还在用object.equals()做断言么?

    在HTTP接口自动化测试时,如果接口返回是JSON格式的结果,通常可以用Sting比较的方式进行断言,或者是经过反序列化形成对象或者对象数组,通过对象间Equa...

    Antony
  • Swift实践:使用CoreData存储多种数据类的通讯录1. CoreData支持存储数据类型2. 使用CoreData存储多种数据类的通讯录3. Codable

    stanbai

扫码关注云+社区

领取腾讯云代金券