iOS 解码/编码

JSONEncoder / JSONDecoder

一个类型通过声明自己遵守 Encodable 和/或 Decodable 协议,来表明可以被序列化和/或反序列化。这两个协议都只约束了一个方法,其中:Encodable 约束了 encode(to:),它定义了一个类型如何对自身进行编码;而 Decodable 则约束了一个初始化方法,用来从序列化的数据中创建实例:

/// 一个类型可以将自身编码为某种外部表示形式。
public protocol Encodable {
/// 将值编码到给定的 encoder 中。
public func encode(to encoder: Encoder) throws
}
/// 一个类型可以从某种外部表示形式中解码得到自身。
public protocol Decodable {
/// 从给定的 decoder 中解码来创建新的实例。
public init(from decoder: Decoder) throws
}

Encoding / Decoding

struct SPUserModel: Codable {
    var name: String
    var contact: SPContactModel?
}
struct SPContactModel: Codable {
    var mobileTelephone = ""
    var fixedTelephone = ""
}

Encoding

let models = [SPUserModel(name: "zhangsan", contact: SPContactModel(mobileTelephone: "138xxxxxxxx", fixedTelephone: "010-xxxxxxx")),
              SPUserModel(name: "lisi", contact: SPContactModel(mobileTelephone: "135xxxxxxxx", fixedTelephone: "020-xxxxxxx"))]

do {
    let encoder = JSONEncoder()
    let jsonData = try encoder.encode(models)
    let jsonString = String(decoding: jsonData, as: UTF8.self)
    dump(jsonString)
} catch { }

Decoding

do {
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData)
    dump(decoded)
} catch { }

合成的代码

Coding Keys

SPUserModel 里,编译器会生成一个叫做 CodingKeys 的私有枚举类型。这个枚举包含的成员与结构体中的存储属性一一对应。

private enum CodingKeys: String, CodingKey {
    case name
    case contact
}

encode(to:) 方法

下面是编译器为 SPUserModel 结构体生成的 encode(to:) 方法:

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(contact, forKey: .contact)
}

init(from:) 初始化方法

当我们调用 try decoder.decode([SPUserModel].self, from: jsonData) 时,解码器会按照我们传入的类型 (这里是 [SPUserModel]),使用 Decodable 中定义的初始化方法创建一个该类型的实例。

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    contact = try container.decode(SPContactModel.self, forKey: .contact)
}

手动遵守协议

自定义 Coding Keys

我们可以创建自定义的 CodingKeys 枚举,在这个枚举中,我们可以:

  • 在编码后的输出中,用明确指定的字符串值重命名字段。
  • 将某个键从枚举中移除,以此跳过与之对应字段。

想要设置一个不同的名字,我们需要明确将枚举的底层类型设置为 String。例如, API 数据某个字段 name 更改为与模型不匹配的字段 username,则需要自定义编码键,添加以下代码,枚举 CodingKeys 中包含 SPUserModel 模型中所有的属性,如此则可以正常解码。

如果枚举里没有包含 name 键,因此编码时 name 将会被跳过,只有 contact 会被编码,被跳过的属性必须赋个默认值,不然将会编译失败。

let json = """
[{
    "username": "zhangsan",
    "contact": {"mobileTelephone": "138xxxxxxxx",
        "fixedTelephone": "010-xxxxxxx"
    }
},
{
    "username": "lisi",
    "contact": {"mobileTelephone": "138xxxxxxxx",
        "fixedTelephone": "010-xxxxxxx"
    }
}]
"""
struct SPUserModel: Codable {
    var name = ""
    var contact: SPContactModel?
    
    private enum CodingKeys: String, CodingKey {
        case name = "username"
        case contact
    }
}
struct SPContactModel: Codable {
    var mobileTelephone = ""
    var fixedTelephone = ""
}
do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
    dump(decoded)
} catch { }

自定义的 encode(to:) 和 init(from:) 实现

JSONEncoderJSONDecoder 默认就可以处理可选值。当目标类型中的一个属性是可选值,如果数据中对应的值不存在的话,解码器将会正确地跳过这个属性。如下面的 contact 属性

let json = """
[{
    "name": "zhangsan"
},
{
    "name": "lisi"
}]
"""
struct SPUserModel: Codable {
    var name = ""
    var contact: SPContactModel?
}
struct SPContactModel: Codable {
    var mobileTelephone = ""
    var fixedTelephone = ""
}

如果数据和所期待的形式不同,则解码错误。 比如给 contact 对象一个空json对象

let json = """
[{
    "name": "zhangsan",
    "contact": { }
},
{
    "name": "lisi",
    "contact": { }
}]
"""

error: The data couldn’t be read because it is missing.

do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
} catch {
    //The data couldn’t be read because it is missing.
    print(error.localizedDescription)
}

重载 Decodable 的初始化方法 init(from:),明确地捕获我们所期待的错误,解码器就可以成功地解码这个错误的 JSON 了

struct SPUserModel: Codable {
    var name = ""
    var contact: SPContactModel?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        do {
            self.contact = try container.decodeIfPresent(SPContactModel.self, forKey: .contact)
        } catch DecodingError.keyNotFound {
            self.contact = nil
        }
    }
}
struct SPContactModel: Codable {
    var mobileTelephone = ""
    var fixedTelephone = ""
}

常见的编码任务

让其他人的代码满足 Codable

假如 SPUserModel 中存在并不满足 Codable 协议的类,比如 CLLocationCoordinate2D ,编译器现在会 (正确地) 抱怨说它无法为 SPUserModel 自动生成实现 Codable 的代码,因为它的 coordinate 属性不再是遵从 Codable 的类型了。

struct SPUserModel: Codable {
    var name = ""
    var coordinate: CLLocationCoordinate2D
}

解决办法1

struct SPUserModel: Codable {
    var name = ""
    var coordinate: CLLocationCoordinate2D
    
    private enum CodingKeys: String, CodingKey {
        case name
        case latitude = "lat"
        case longitude = "lon"
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        // 分别编码纬度和经度
        try container.encode(coordinate.latitude, forKey: .latitude)
        try container.encode(coordinate.longitude, forKey: .longitude)
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        // 从纬度和经度重新构建 CLLocationCoordinate2D
        self.coordinate = CLLocationCoordinate2D (
            latitude: try container.decode(Double.self, forKey: .latitude),
            longitude: try container.decode(Double.self, forKey: .longitude)
        )
    }
}
let json = """
[{
    "name": "zhangsan",
    "lat": 312312313,
    "lon": 3452423424
},
{
    "name": "lisi",
    "lat": 123132343,
    "lon": 3453432423
}]
"""

do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
    //Optional(__C.CLLocationCoordinate2D(latitude: 312312313.0, longitude: 3452423424.0))
    dump(decoded)
} catch { }

解决办法2:嵌套容器

struct SPUserModel: Codable {
    var name = ""
    var coordinate: CLLocationCoordinate2D
    
    private enum CodingKeys: String, CodingKey {
        case name
        case coordinate
    }
    
    // 嵌套容器的编码键
    private enum CoordinateCodingKeys: CodingKey {
        case latitude
        case longitude
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        var coordinateContainer = container.nestedContainer(keyedBy: CoordinateCodingKeys.self, forKey: .coordinate)
        try coordinateContainer.encode(coordinate.latitude, forKey: .latitude)
        try coordinateContainer.encode(coordinate.longitude, forKey: .longitude)
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        let coordinateContainer = try container.nestedContainer(keyedBy: CoordinateCodingKeys.self, forKey: .coordinate)
        self.coordinate = CLLocationCoordinate2D (
            latitude: try coordinateContainer.decode(Double.self, forKey: .latitude),
            longitude: try coordinateContainer.decode(Double.self, forKey: .longitude)
        )
    }
}
let json = """
[{
    "name": "zhangsan",
    "coordinate": {
        "latitude": 279886268,
        "longitude": 123678613
                  }
},
{
    "name": "lisi",
    "coordinate": {
        "latitude": 221311,
        "longitude": 67868
                  }
}]
"""

do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
    //Optional(__C.CLLocationCoordinate2D(latitude: 279886268.0, longitude: 123678613.0))
    dump(decoded)
} catch { }

解决办法3

struct SPCoordinate: Codable {
    var latitude: Double
    var longitude: Double
}

struct SPUserModel: Codable {
    
    var name: String
    private var _coordinate: SPCoordinate
    var coordinate: CLLocationCoordinate2D {
        get {
            return CLLocationCoordinate2D(latitude: _coordinate.latitude,
                                          longitude: _coordinate.longitude)
        }
        set {
            _coordinate = SPCoordinate(latitude: newValue.latitude,
                                     longitude: newValue.longitude)
        }
    }
    private enum CodingKeys: String, CodingKey {
        case name
        case _coordinate = "coordinate"
    }
}
let json = """
[{
    "name": "zhangsan",
    "coordinate": {
        "latitude": 279886268,
        "longitude": 123678613
                  }
},
{
    "name": "lisi",
    "coordinate": {
        "latitude": 221311,
        "longitude": 67868
                  }
}]
"""

do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
    //Optional(__C.CLLocationCoordinate2D(latitude: 279886268.0, longitude: 123678613.0))
    dump(decoded)
} catch { }

tips

1. 属性样式转换(mobileTelephone -> mobile_telephone)

假如 API 数据某个字段 mobileTelephone 更改为 mobile_telephone 样式,则会出现错误 :

let json = """
[{
    "name": "zhangsan",
    "contact": {"mobile_telephone": "138xxxxxxxx",
        "fixed_telephone": "010-xxxxxxx"
    }
},
{
    "name": "lisi",
    "contact": {"mobile_telephone": "138xxxxxxxx",
        "fixed_telephone": "010-xxxxxxx"
    }
}]
"""

do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
    print(decoded.first?.contact?.fixedTelephone)
} catch {
    //error: The data couldn’t be read because it is missing.
    print(error.localizedDescription)
}
struct SPUserModel: Codable {
    var name: String
    var contact: SPContactModel?
}
struct SPContactModel: Codable {
    var mobileTelephone = ""
    var fixedTelephone = ""
}

在需要编码和解码的地方添加以下代码

encoder.keyEncodingStrategy = .convertToSnakeCase
decoder.keyDecodingStrategy = .convertFromSnakeCase

2.嵌套类型改变

假如 API 数据嵌套类型更改,而我们仍然希望使用嵌套。

let json = """
[{
    "name": "zhangsan",
    "mobile_telephone": "138xxxxxxxx",
    "fixed_telephone": "010-xxxxxxx"
},
{
    "name": "lisi",
    "mobile_telephone": "138xxxxxxxx",
    "fixed_telephone": "010-xxxxxxx"
}]
"""

do {
    let jsonData = json.data(using: .utf8)
    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase
    let decoded = try decoder.decode([SPUserModel].self, from: jsonData!)
} catch { }
struct SPUserModel: Codable {
    var name: String
    var contact: SPContactModel?
    
    private enum CodingKeys: CodingKey {
        case name
        case mobileTelephone
        case fixedTelephone
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(contact?.mobileTelephone, forKey: .mobileTelephone)
        try container.encode(contact?.fixedTelephone, forKey: .fixedTelephone)
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
        self.contact = SPContactModel (
            mobileTelephone: try container.decode(String.self, forKey: .mobileTelephone),
            fixedTelephone: try container.decode(String.self, forKey: .fixedTelephone)
        )
    }
}
struct SPContactModel: Codable {
    var mobileTelephone = ""
    var fixedTelephone = ""
}

3.日期的编解码

encoder.dateEncodingStrategy = .formatted(<#T##DateFormatter#>)
decoder.dateDecodingStrategy = .formatted(<#T##DateFormatter#>)

假如我们希望日期的格式为 "yyyy-MM-dd"

extension DateFormatter {
  static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    return formatter
  }()
}
struct SPUserModel: Codable {
    var name: String
    var birthday: Date?
}

需要设置解编码的 dateEncodingStrategy 属性

let models = [SPUserModel(name: "zhangsan", birthday: Date()),
              SPUserModel(name: "lisi", birthday: Date())]
        
do {
    let encoder = JSONEncoder()
    encoder.dateEncodingStrategy = .formatted(.dateFormatter)
    let jsonData = try encoder.encode(models)
    let jsonString = String(decoding: jsonData, as: UTF8.self)
} catch { }

do {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .formatted(.dateFormatter)
    let decoded = try decoder.decode([SPUserModel].self, from: self.jsonData)
} catch { }

PropertyListEncoder / PropertyListDecoder

class SPUserModel: NSObject, Codable {
    var name: String
    var address: String
    
    init(name: String, address: String) {
        self.name = name
        self.address = address
    }
}

Encoding

let models = [SPUserModel(name: "zhangsan", address: "beijing"),
              SPUserModel(name: "lisi", address: "shanghai")]

do {
    let data = try PropertyListEncoder().encode(models)
    let data2 = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true)
    UserDefaults.standard.set(data2, forKey: "key")
} catch { }

Decoding

guard let data = UserDefaults.standard.object(forKey: "key") else { return }
do {
    let data2 = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [SPUserModel.self], from: data as! Data)
    let model = try PropertyListDecoder().decode([SPUserModel].self, from: data2 as! Data)
    dump(model)
} catch  { }

--- ---

Encoding

let models = [SPUserModel(name: "zhangsan5", address: "beijing"),
              SPUserModel(name: "lisi3", address: "shanghai")]

do {
    let data = try PropertyListEncoder().encode(models)
    NSKeyedArchiver.archiveRootObject(data, toFile: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! + "/info")
} catch { }

Decoding

guard let data = NSKeyedUnarchiver.unarchiveObject(withFile: NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! + "/info") else { return }
do {
    let model = try PropertyListDecoder().decode([SPUserModel].self, from: data as! Data)
} catch  { }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342