如何用原生的方式来定义Swift JSON Model

在Swift开发中,处理JSON数据序列化是一项常见任务。由于Swift的类型安全特性,处理类似JSON这样的弱类型数据一直是一个挑战。然而,Swift 4引入了一个令人欣喜的特性,即Codable协议。Codable协议为我们提供了一种简洁的方式来序列化和反序列化JSON数据。

尽管Codable协议在处理大多数情况下表现得很出色,但它并不能完全满足所有需求。例如,它不支持自动类型转换,也无法友好地处理默认值。

如果我们能解决这些问题,就能更完美地处理JSON数据了。我们可以自定义解码器和编码器,以提供更高级的功能。通过自定义解码器,我们可以实现类型的自动转换,将JSON数据转换为目标类型,而无需手动处理。此外,我们还可以通过自定义编码器,在编码过程中为属性设置默认值,以确保生成的JSON数据符合预期。

总之,通过充分利用Swift的特性和自定义解码器、编码器,我们可以更好地处理JSON数据,满足我们更复杂的需求。 传送门ObjMapper。

Codable坑点1:不支持类型转换

kotlin 复制代码
// JSON:
{
    "uid":"123456",
    "name":"Harry",
    "age":10
}

// Model:
struct Dog: Codable{
    var uid: Int
    var name: String?
    var age: Int?
}

在json转换过程中,我们常常与遇到类型模型与json的类型不一致的情况,就像上面的uid字段,uid在json中是String,但是我们的模型是Int,由于swift是类型安全的,所以,转换就不会成功。

Codable坑点2:不支持默认值

话不多说,上代码

objc 复制代码
struct Activity: Codable {
    enum Status: Int {
        case start = 1//活动开始
        case processing = 2//活动进行中
        case end = 3//活动结束
    }
    
    var name: String
    var status: Status//活动状态
}

这儿有一个活动,活动现目前有三种状态,到目前为止,一切都很美好。有一天,突然说需要给活动添加已下架的状态,what?

json 复制代码
//JSON
{
    "name": "元旦迎新活动",
    "status": 4
}

用Activity解析上面的JSON就会报错,我们如何规避呢,像下面一样

csharp 复制代码
var status: Status?

答案是no、no、no,因为可选值的解码所表达的是"如果不存在,则置为 nil",而不是"如果解码失败,则置为 nil"。

解决方案

有没有更好的方式来处理上面这两个问题呢?具体代码见ObjMapper,这儿简单描述下如何使用。

1、Model与JSON相互转换

objc 复制代码
// JSON:
{
    "uid":888888,
    "name":"Tom",
    "age":10
}

// Model:
struct Dog: Codable{
    //如果字段不是可选类型,则使用Default,提供一个默认值,像下面一样
    @Default<Int.Zero> var uid: Int
    //如果是可选类型,则使用Backed
    @Backed var name: String?
    @Backed var age: Int?
}

//JSON to model
let dog = Dog.decodeJSON(from: json)

//model to json
let json = dog.jsonString

当 JSON/Dictionary 中的对象类型与 Model 属性不一致时,ObjMapper 将会进行如下自动转换。自动转换不支持的值将会被设置为nil或者默认值。

JSON/Dictionary Model
String String,Number类型(包含整数,浮点数),Bool
Number类型(包含整数,浮点数) Number类型,String,Bool
Bool Bool,String,Number类型(包含整数,浮点数)
nil nil,0

2、Model的嵌套

objc 复制代码
let raw_json = """
{
    "author":{
        "id": 888888,
        "name":"Alex",
        "age":"10"
    },
    "title":"model与json互转",
    "subTitle":"如何优雅的转换"
}
"""

// Model:
struct Author: Codable{
    @Default<Int.Zero> var id: Int
    @Default<String.Empty> var name: String
    //使用Backed后,如果类型不匹配,则类型会自动转换
    //比如,上面的json中,age是个字符串,我们定义的模型是Int,
    //那么声明@Backed后,会自动转换成Int类型
    @Backed var age: Int?
}

struct Article: Codable {
    //如果json中的title为nil或者不存在,则会给title赋一个默认值
    @Default<String.Empty> var title: String
    var subTitle: String?
    var author: Author
}

//JSON to model
let article = Article.decodeJSON(from: raw_json)

//model to json
let json = article.jsonString
print(article?.jsonString ?? "")

3、自定义类型的可选值

话不多说,上代码

objc 复制代码
struct Activity: Codable {
    enum Status: Int {
        case start = 1//活动开始
        case processing = 2//活动进行中
        case end = 3//活动结束
    }
    
    @Default<String.Empty> var name: String
    var status: Status//活动状态
}

这儿有一个活动,活动现目前有三种状态,到目前为止,一切都很美好。有一天,突然说需要给活动添加已下架的状态,what?

json 复制代码
//JSON
{
    "name": "元旦迎新活动",
    "status": 4
}

用Activity解析上面的JSON就会报错,我们如何规避呢,像下面一样

csharp 复制代码
var status: Status?

答案是no、no、no,因为可选值的解码所表达的是"如果不存在,则置为 nil",而不是"如果解码失败,则置为 nil",那就用我们的Default吧,请看下面代码:

swift 复制代码
struct Activity: Codable {
    ///Step 1:让Status遵循DefaultValue协议
    enum Status: Int, Codable, DefaultValue {
        case start = 1//活动开始
        case processing = 2//活动进行中
        case end = 3//活动结束
        case unknown = 0//默认值,无意义
        
        ///Step 2:实现DefaultValue协议,指定一个默认值
        static func defaultValue() -> Status {
            return Status.unknown
        }
    }
    
    @Default<String.Empty> var name: String
    ///Step 3:使用Default
    @Default<Status> var status: Status//活动状态
}

//{"name": "元旦迎新活动", "status": 4 }
//Activity将会把status解析成unknown

4、为普通类型设置不一样的默认值

本库已经内置了很多默认值,比如Int.Zero, Bool.True, String.Empty...,如果我们想为字段设置不一样的默认值,见下面代码:

less 复制代码
public extension Int {
    enum One: DefaultValue {
        static func defaultValue() -> Int {
            return 1
        }
    }
}

struct Dog: Codable{
    @Backed var name: String?
    @Default<Int.Zero> var uid: Int
    //如果json中没有age字段或者解析失败,则模型的age被设置成默认值1
    @Default<Int.One> var age: Int
}

5、数组支持

对于数组,可以使用@Backed,@Default来解析

objc 复制代码
// JSON:
let raw_json = """
{
    "code":0,
    "message":"success",
    "data": [{
        "name": "元旦迎新活动",
        "status": 4
    }]
}
"""

struct Activaty: Codable{
    @Default<String.Empty> var name: String
    @Default<Int.Zero> var status: Int
}

// 如果数组是可选类型,可以使用@Backed
struct Response1: Codable {
    @Default<Int.Zero> var code: Int
    @Default<String.Empty> var message: String
    @Backed var data: [Activaty]?
}

// 为数组,设置默认值,如果数组不存在或者解析错误,则使用默认值
struct Response2: Codable {
    @Default<Int.Zero> var code: Int
    @Default<String.Empty> var message: String
    @Default<Array.Empty> var data: [Activaty]
}
//JSON to model
let rsp1 = Response1.decodeJSON(from: raw_json)
let rsp2 = Response2.decodeJSON(from: raw_json)

//model to json
let json1 = rsp1.jsonString
let json2 = rsp2.jsonString
// print(rsp1?.jsonString ?? "")
// print(rsp2?.jsonString ?? "")

6、设置通用类型

我们在开发过程中,第一个遇到的json可能是这样的:

objc 复制代码
// JSON:
{
    "code":0,
    "message":"success",
    "data":[]//这个data可以是任何类型
}

由于data字段的类型不固定,有时候为了统一处理,我们定义模型可以像下面这样,有枚举类型JsonValue来表示。

kotlin 复制代码
struct Response: Codable { 
    var code: Int
    var message: String
    var data: JsonValue?
}

如果要取data字段的值,我们可以这样用data?.intValue或者data?.arrayValue等等,具体使用见源码。

注意:这种对于data是一个简单的model(比如就是一个整形、字符串等等),可以起到事半功倍的效果;如果data是一个大型model,建议还是将data指定为具体类型。

7、如果是从1.0.x升级到2.0版本,修改了DefaultValue协议。如果之前的代码中使用了DefaultValue协议,则会报错,修改如下:

csharp 复制代码
原来为:
///Step 1:让Status遵循DefaultValue协议
enum Status: Int, Codable, DefaultValue {
    case start = 1//活动开始
    
    ///Step 2:实现DefaultValue协议,指定一个默认值
    static let defaultValue = Status.unknown
}

修改成:
///Step 1:让Status遵循DefaultValue协议
enum Status: Int, Codable, DefaultValue {
    case start = 1//活动开始
    
    ///Step 2:实现DefaultValue协议,返回一个默认值
    static func defaultValue() -> Status {
        return Status.unknown
    }
}

参考文档

  1. 用 Codable 协议实现快速 JSON 解析
  2. Swift 4 踩坑之 Codable 协议
  3. 使用 Property Wrapper

不喜勿喷,有问题请留言😁😁😁,欢迎✨✨✨star✨✨✨和PR

相关推荐
茶底世界之下7 小时前
导出进度不是一个百分比:AVFoundation 收尾阶段的状态建模
ios·swift
大熊猫侯佩7 小时前
macOS27 上可变界面尺寸 iOS App 揭秘
macos·ios·swiftui
用户2181697049307 小时前
LLVM
ios
茶底世界之下7 小时前
为什么媒体源不能在回调仍执行时提前报告 finish?
swift
zeqinjie7 小时前
Flutter 获取 iPhone Duo 预留区位置
前端·flutter·ios
二流小码农7 小时前
鸿蒙开发:ArrayList,可不会让UI更新哦
android·ios·harmonyos
茶底世界之下7 小时前
为什么视频处理器必须提前声明“它能处理什么帧”
swift
茶底世界之下8 小时前
录制结束,不代表视频文件已经交付:AVFoundation 录制完成状态的工程化判断
swift
茶底世界之下6 天前
为什么预览、录制、离线导出不能共享同一个背压策略?
ios·swift
2501_916007476 天前
苹果商店iOS应用上架费用全面解析:开发者计划与审核费用计算
android·ios·小程序·https·uni-app·iphone·webview