Codable 基本使用

Codabel JSON转模

官方文档:Using JSON with Custom Types

JSON 介绍:JavaScript Object Notation

Codable 是 Swift 引入的全新的编解码库,使开发者更方便的解析JSON 或 plist 文件。支持枚举、结构体和类。

协议定义如下:

typealias Codable = Decodable & Encodable

public protocol Decodable {
    
    
    public init(from decoder: Decoder) throws
}
public protocol Encodable {
    
    
    public func encode(to encoder: Encoder) throws
}

解析 JSON

对于 JSON的编码和解析,使用JSONEncoder用于编码为 data,使用JSONDecoder用于解析为Model。

let data = try! JSONEncoder().encode(模型实例)               // 编码
let model = try! JSONDecoder().decode(模型.self, from: data)// 解码

示例json

///  json数据
let json = """
[
    {
    
    
        "name": "Banana",
        "points": 200,
        "description": "A banana grown in Ecuador."
    },
    {
    
    
        "name": "Orange",
        "points": 100
    }
]
"""

遵循 Codable 定义模型

/// 模型
struct GroceryProduct: Codable {
    
    
    var name: String
    var points: Int
    var description: String?
}

JSON 解析为Model

// 开始转换
let decoder = JSONDecoder()
// 数组模型转化  [注意:先将json字符串转为data,同时注意捕获异常]
let products = try decoder.decode([GroceryProduct].self, from: json.data(using:.utf8)!)

Codable 支持枚举

原始值类型枚举

枚举通常有两种变体:

  • 原始值(如IntString)支持的变体
  • 包含关联值的变体。自从在Swift 4.0中引入Codable以来,属于前一类别的枚举一直支持编译器合成。

因此,例如,假设我们正在开发一个包含以下String支持枚举的应用程序,该枚举符合Codable

enum MediaType: String, Codable {
    
    
    case article
    case podcast
    case video
}

由于编译器能够自动处理使用原始值对枚举进行编码和解码所需的所有代码,我们通常不必编写更多的代码——这意味着我们现在可以在其他Codable类型中使用上述枚举,例如:

struct Item: Codable {
    
    
    var title: String
    var url: URL
    var mediaType: MediaType
}

如果我们现在将上述Item类型的实例编码为JSON,那么我们将获得以下类型的输出(因为MediaType值将使用支持它们的原始String值自动编码和解码):

{
    
    
    "title": "Swift by Sundell",
    "url": "https://swiftbysundell.com/podcast",
    "mediaType": "podcast"
}

关联值枚举

在Swift 5.5之前,如果我们想使包含关联值的枚举符合Codable,那么我们必须手动编写所有代码。然而,swift5.5不再是这样了,因为编译器已经升级,现在它也能够自动生成此类枚举的序列化代码。

例如,以下Video枚举现在可以Codable,而无需我们提供任何自定义代码:

enum Video: Codable {
    
    
    case youTube(id: String)
    case vimeo(id: String)
    case hosted(url: URL)
}

要查看上述类型在编码时是什么样子的,让我们创建一个VideoCollection实例,该实例存储Video值数组:

struct VideoCollection: Codable {
    
    
    var name: String
    var videos: [Video]
}

let collection = VideoCollection(
    name: "Conference talks",
    videos: [
    .youTube(id: "ujOc3a7Hav0"),
    .vimeo(id: "234961067")
]
)

如果我们然后将上述collection值编码为JSON,那么我们将获得以下结果:

{
    
    
    "name": "Conference talks",
    "videos": [
        {
    
    
            "youTube": {
    
    
    "id": "ujOc3a7Hav0"
}
        },
        {
    
    
            "vimeo": {
    
    
    "id": "234961067"
}
        }
    ]
}

因此,默认情况下,当我们让编译器自动合成带有关联值的枚举的Codable一致性时,在计算该类型的序列化格式时,将使用我们案例的名称及其中关联值的标签。

如果我们想自定义关联值枚举 key,还是通过自定义``CodingKey`,可以参考Codable synthese for Swift enums

进行键转换

如果在后台系统使用的命名规则与前端不一致情况下,我们就需要进行键自定义转换。比如后台字段返回下划线命名法,需要转换为驼峰命名法,所以在字段映射的时候就需要修改一下。

主要有两种方式

  1. 【推荐】实现CodingKey协议 进行枚举映射。
  2. 通过Decoder的keyDecodingStrategy中convertFromSnakeCase统一转化
/// 示例json
let json = """
[
    {
    
    
        "product_name": "Bananas",
        "product_cost": 200,
        "description": "A banana grown in Ecuador."
    },
    {
    
    
        "product_name": "Oranges",
        "product_cost": 100,
        "description": "A juicy orange."
    }
]
""".data(using: .utf8)!
/// 对象模型
struct GroceryProduct: Codable {
    
    
    var name: String
    var points: Int
    var description: String?
    
    /// 自定义字段属性
    /// 注意 1.需要遵守Codingkey  2.每个字段都要枚举
    private enum CodingKeys: String, CodingKey {
    
    
        case name = "product_name"
        case points = "product_cost"
        case description
    }
}

let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)

下划线转化与自定义

某些时候后台使用的是下划线命名法(比如mysql和python),返回的字段名就是下滑线的。swift4.1之后可以自动将下划线命名转化为驼峰命名法。

当然也可以用自定义属性来改变,但是如果字段过多怎很麻烦。

/// 示例json
let json = """
[
    {
    
    
        "product_name": "Bananas",
        "product_cost": 200,
        "description": "A banana grown in Ecuador."
    },
    {
    
    
        "product_name": "Oranges",
        "product_cost": 100,
        "description": "A juicy orange."
    }
]
""".data(using: .utf8)!
/// 对象模型
struct GroceryProduct: Codable {
    
    
    var productName: String
    var productCost: Int  // 不允许部分转化 部分不转 如product_cost会报错异常
    var description: String?
}

let decoder = JSONDecoder()
/// 通过keyDecodingStrategy 来控制
decoder.keyDecodingStrategy = .convertFromSnakeCase  // 编码策略  使用从蛇形转化为大写 encode时同样也可降驼峰命名法转化为下划线
let products = try decoder.decode([GroceryProduct].self, from: json)

自定义转换规则

字段转化策略除了使用默认的和下划线转驼峰之外,还可以自定义转化规则,比如下面示例,将 key 大写映射到小写。

/// 定义模型
struct Address: Codable {
    
    
    var street: String
    var zip: String
    var city: String
    var state: String
}

/// 重点  实现CodingKey协议
struct AnyKey: CodingKey {
    
    
    var stringValue: String
    var intValue: Int?

    init?(stringValue: String) {
    
    
        self.stringValue = stringValue
    }

    init?(intValue: Int) {
    
    
        self.stringValue = String(intValue)
        self.intValue = intValue
    }
}


转化模型
let jsonString = """
{
    
    "State":"California","Street":"Apple Bay Street","Zip":"94608","City":"Emeryville"}
"""

let decoder = JSONDecoder()
/// 建议custom 使用扩展KeyDecodingStrategy [方便管理]
decoder.keyDecodingStrategy = .custom({
    
     (keys) -> CodingKey in
    // 转化规则
    let lastKey = keys.last!
    guard lastKey.intValue == nil else {
    
     return lastKey }
    let stringValue = lastKey.stringValue.prefix(1).lowercased() + lastKey.stringValue.dropFirst()  /// 将首字母大写的转化为小写的
    return AnyKey(stringValue: stringValue)!
})

if let jsonData = jsonString.data(using: .utf8), let address = try? decoder.decode(Address.self, from: jsonData) {
    
    
    print(address)
}

/*
 prints:
 Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")
 */

参考链接:keyDecodingStrategy.customXXX的使用 – StackOverflow

重写Decode 和Encode 方法

Decode 协议和 Encode 协议有默认方法,所以我们可以实现其方法,在编码和解码时进行自定义操作。如多级嵌套字段属性转化。

如下 json 直接获取二级字段作为属性

/// 示例json
let json = """
{
    
    
    "order":
    {
    
    
        "product_name": "Bananas",
        "product_cost": 10,
        "description": null
    }
}
""".data(using: .utf8)!

模型定义,指明对应的CodingKey, 实现Encodable 和Decodable协议方法

/// 对象模型
struct GroceryProduct: Codable {
    
    
    var productName: String  // 第二层字段
    var productCost: Int     // 第二层字段
    
    /// 定义第一层嵌套 编码键
    private enum OrderKeys: CodingKey {
    
    
        case order
    }
    /// 定义第二层嵌套 编码键
    private enum CodingKeys: String, CodingKey {
    
    
        case productName
        case productCost
    }
    /// 实现 Decodable 协议 【tip: 代码提示不会显示init方法,建议进入协议内复制过来】
    /// 实现 键与属性的映射赋值
    init(from decoder: Decoder) throws{
    
    
        // 获取对应的容器
        let orderContainer = try decoder.container(keyedBy: OrderKeys.self)
        let container = try orderContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: OrderKeys.order)
        
        // 对属性赋值
        productName = try container.decode(String.self, forKey: .productName)
        productCost = try container.decode(type(of: productCost), forKey: .productCost)
    }
    
    func encode(to encoder: Encoder) throws{
    
    
        var orderContainer =  encoder.container(keyedBy: OrderKeys.self)
        var container = orderContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: OrderKeys.order)
        try container.encode(productName, forKey: .productName)
        try container.encode(productCost, forKey: .productCost)
    }
    
}

JSON 与Model 转换

let decoder = JSONDecoder()
/// 通过keyDecodingStrategy 来控制
decoder.keyDecodingStrategy = .convertFromSnakeCase
let products = try decoder.decode(GroceryProduct.self, from: json)

// 模型对象转化为json
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.keyEncodingStrategy = .convertToSnakeCase
let jsonStr = try! encoder.encode(products)

print("模型转json:\(String.init(data: jsonStr, encoding: .utf8))")
//模型转json:Optional("{\n  \"order\" : {\n    \"product_cost\" : 10,\n    \"product_name\" : \"Bananas\"\n  }\n}")

注意事项

模型默认值问题

对于模型的定义,我们有时候希望设置默认值,然而 Codable 却不支持,如果字段不存在或者为 nil,解析时候默认值会崩溃(我也会崩溃,因为一般无法保证后台一定会有该字段)。

"No value associated with key CodingKeys(stringValue: \"description\", intValue: nil) (\"description\")."

这时候建议使用可选属性。

/// 示例json
let json = """
    {
    
    
        "product_name": "Bananas",
        "product_cost": 10,
        "description": null
    }
""".data(using: .utf8)!
/// 对象模型
struct GroceryProduct: Codable {
    
    
    var productName: String
    var productCost: Int
    var description: String?  /// 不能确定一定会返回值或者可能返回null 时 建议使用可选
}

let decoder = JSONDecoder()
/// 通过keyDecodingStrategy 来控制
decoder.keyDecodingStrategy = .convertFromSnakeCase  
let products = try decoder.decode(GroceryProduct.self, from: json)

万一我就是想添加初始值咋办:CleanJSON , 或者设置该属性为private 新增一个计算下属性外部访问。

类继承

如果你使用类继承,会发现父类属性无法自动解析。

复杂的解决方法,重写init(from decoder: Decoder)方法,内部自行解析子类属性和调用父类解析方法。

简单方法,未知?

let jsonData = """
{
    
    
  "name": "李四",
  "age": 18,
  "address": "地球"
}
""".data(using: .utf8)!

class CodablePart: Codable {
    
    
    let name: String
    let age: UInt
    
}

class WholeModel: CodablePart {
    
    
    let address: string
    required init(from decoder: Decoder) throws {
    
    
        let container = try decoder.container(keyedBy: CodingKeys.self)
        address = try container.decode(Int.self, forKey: .uncodableNumber) // 设置子类新增属性
        try super.init(from: decoder)  // 需要调用父类解析解析方法
    }
}
extension WholeModel {
    
    
  enum CodingKeys: String, CodingKey {
    
    
        case address
    }
}
// 解析为 model
do {
    
    
    let model = try JSONDecoder().decode(WholeModel.self, from: jsonData)
    print(model.name)  // 李四
    print(model.age)   // 18
    print(model.address)// 地球
} catch {
    
    
    print(error.localizedDescription)
}

使用 Tips

屏蔽部分属性解析

可以使用 lazy 懒加载方式,既可以赋初始值,也使用了懒加载方式,还可以标记属于业务属性。我们还可以通过 private影藏后台属性,定义计算属性作为业务属性使用。

struct Person: Codable {
    
    
        var name: String?
        var gender: Int?
        var address: String?
        /// 如果json中没有该属性,但是自己想用,可以添加lazy并初始化
        lazy var isSelected:Bool = false
    }

扩展转为 Data 和 字典

扩展 Data

extension Data {
    
    
    /// 从遵循`Encodable`的实例转换为 `Data`
    /// - Parameter model:遵循`Encodable`的实例
    init?<T: Encodable>(from model: T) {
    
    
        guard let modelData =  try? JSONEncoder().encode(model) else {
    
    
            return nil
        }
        self = modelData
    }
}

扩展字典

extension Dictionary where Key == String, Value == Any {
    
    
    /// 从遵循`Encodable`的实例转换为 `Dictionary`,字典类型为:`[String: Any]`
    /// - Parameter model:遵循`Encodable`的实例
    init?<T: Encodable>(from model: T) {
    
    
        if let data = Data(from: model) {
    
    
            do {
    
    
                guard let dic = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
    
    
                    debugPrint("model can not transform [String: Any]")
                    return nil
                }
                self = dic
            } catch {
    
    
                debugPrint(error.localizedDescription)
                return nil
            }
        } else {
    
    
            debugPrint("model to data error")
            return nil
        }
    }
}

相关库推荐

CleanJSON:【推荐】解决通过自定义 Decoder 方式解决最大痛点(不能设置默认值问题),且代码入侵非常小。

Kodable:通过 ProperWrapper 方式提供以下功能(会对模型进行代码入侵)

  • 无需编写自己的init(from decoder: Decoder)CodingKeys
  • 提供自定义密钥进行解码
  • 使用.符号访问嵌套值
  • 添加默认值,以防缺少该值
  • 重写解码的值(即修剪字符串)
  • 验证解码的值
  • 自动尝试从其他类型解码StringBool作为回退
  • 变压器协议,在现有功能的基础上实现您自己的额外功能

猜你喜欢

转载自blog.csdn.net/qq_14920635/article/details/120992262
今日推荐