有關Swift Codable解析成Dictionary的一些事
theme: cyanosis
前言
假設有一json:
json
{
"int": 1234,
"string": "測試",
"double": 10.0002232,
"ext": {
"intExt": 456,
"stringExt": "擴充套件",
"doubleExt": 456.001
}
}
日常開發中,常見的會將資料實體確定好,然後再解析:
```swift struct TestCodable: Codable { let int: Int let string: String let double: Double let ext: Ext }
struct Ext: Codable {
let intExt: Int
let stringExt: String
let doubleExt: Double
}
但正如json串中命名的`ext`,這是個擴充套件屬性,意味著需求應該是會在該欄位中**存在一些動態欄位**。這時,其實我們更希望將它解析成一個`Dictionary<String, Any>`字典型別
swift
struct TestCodable: Codable {
let int: Int
let string: String
let double: Double
let ext: [String: Any]
}
``
遺憾的是,由於
Dictionary型別在
Codable是**沒有預設解析的邏輯**,或者說
Any這個不確定型別**無法直接用
Codable解析**,所以上述的程式碼其實是會報錯的。但這種做法,對於一些擁有靈活配置的需求來說,又是非常的合理。因此,本文就重點結合這一合理的需求**聊聊如何採用
Codable庫來解析
Dictionary,還有在研究過程中發現它與
GRDB`的不相容性以及處理方案**。
Codable解析
使用
```swift let json = """ { "int": 1234, "string": "測試", "double": 10.0002232 } """ // json -> 實體 let jsonData = json.data(using: .utf8)! let jsonDecoder = JSONDecoder.init() let a = try! jsonDecoder.decode(A.self, from: jsonData)
// 實體 -> json
let jsonEncoder = JSONEncoder.init()
let jsonData2 = try! jsonEncoder.encode(testCodable)
let json2 = String.init(data: jsonData2, encoding: .utf8)!
``
上述程式碼是利用
Codable提供的
JSONDecoder將json串解析成資料實體和利用
JSONEncoder`將資料實體打包成json串的過程。
```swift struct A: Codable { let int: Int let string: String let double: Double
enum Key: String, CodingKey {
case int
case string
case double
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: Key.self)
try container.encode(int, forKey: .int)
try container.encode(string, forKey: .string)
try container.encode(double, forKey: .double)
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
self.int = try container.decode(Int.self, forKey: .int)
self.string = try container.decode(String.self, forKey: .string)
self.double = try container.decode(Double.self, forKey: .double)
}
}
``
Codable的
encode和
decode都會通過實體的
func encode(to encoder: Encoder)和
init(from decoder: Decoder)`,如果開發者自己不自定義的話,編譯器會自動生成。
為什麼不能解析成Any型別
回到開頭說的例子,假如我們定義成Dictionary<String, Any>
的話會報以下錯誤
swift
struct TestCodable: Codable {
let int: Int
let string: String
let double: Double
// Error: Type 'TestCodable' does not conform to protocol 'Decodable'
// Error: Type 'TestCodable' does not conform to protocol 'Encodable'
let ext: [String: Any]
}
字面理解就是,由於[String: Any]
的定義,導致Codable
無法自動生成編解碼的定義,也就是說Codable
只能支援自身定義好的解析,如整型或者是繼承了Codable
的型別,而Any
是一個例外。
```swift
public mutating func encode(_ value: Int, forKey key: KeyedEncodingContainer
public mutating func encode
public func decode(_ type: Int.Type, forKey key: KeyedDecodingContainer
public func decode
所以解決問題的核心就是需要一個支援Dictionary<String, Any>
的encode
和decode
方法。
原始碼分析
JSONEncoder、JSONDecoder原始碼:
CodingKey
當我們呼叫JSONDecoder#decode
方法時,內部其實是利用JSONSerialization
將Json轉換為字典的。
```swift
// JSONDecoder
open func decode
let decoder = __JSONDecoder(referencing: topLevel, options: self.options)
guard let value = try decoder.unbox(topLevel, as: type) else {
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))
}
return value
}
以`KeyedDecodingContainer`為例,字典最終會**儲存在`_JSONKeyedDecodingContainer#container`當中**。
swift
private struct _JSONKeyedDecodingContainer
// MARK: Properties
/// A reference to the decoder we're reading from.
private let decoder: __JSONDecoder
/// A reference to the container we're reading from.
private let container: [String : Any]
/// The path of coding keys taken to get to this point in decoding.
private(set) public var codingPath: [CodingKey]
所以理論上,**Json當中的所有欄位,都可以從`container`中取出**。而在`decode`方法中,這裡以Bool型解析為例:
swift
public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
guard let entry = self.container[key.stringValue] else {
throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key (_errorDescription(of: key))."))
}
self.decoder.codingPath.append(key)
defer { self.decoder.codingPath.removeLast() }
guard let value = try self.decoder.unbox(entry, as: Bool.self) else {
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead."))
}
return value
}
`decode`方法中,會**通過`Key#stringValue`作為`key`取出`container`內對應的`value`**,而這裡的`Key`即為`CodingKey`型別,結合開頭的例子**可以定義為一個繼承自`CodingKey`的型別**,這裡可以是一個`enum`型別。
swift
public protocol CodingKey : CustomDebugStringConvertible, CustomStringConvertible, Sendable {
var stringValue: String { get }
init?(stringValue: String)
var intValue: Int? { get }
init?(intValue: Int)
}
enum Key: String, CodingKey {
case int
case string
case double
case ext
}
這樣做的好處是,**明確知道Json中有哪些欄位,可以根據特定的key解析出value**,繼而生成實體。但**侷限性就是隻能解析明確了的key值**,明顯不符合本文需求。所以可以稍微做一下變形,實現一個繼承自`CodingKey`的普通型別:
swift
struct JSONCodingKeys: CodingKey {
var stringValue: String
init(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
``
這樣就可以將
_JSONKeyedDecodingContainer#container中的
key`都表示出來。
獲取字典中所有的key
swift
// _JSONKeyedDecodingContainer
public var allKeys: [Key] {
return self.container.keys.compactMap { Key(stringValue: $0) }
}
allKeys
可以從_JSONKeyedDecodingContainer
物件中獲取到內部通過Json解析到的字典裡的key值,前提是Key
型別要支援。意思是
- 假如我們定義的Key
是一個enum
型別,那麼返回的集合就只能包含在enum
中定義的值,如:Key.int
、Key.string
等。
- 假如我們定義的Key
是一個正常的struct
或class
,那就意味著字典中的全部key
都會轉換成這個型別的物件,如:JSONCodingKeys.init("int")
、JSONCodingKeys.init("string")
等。
解析[String: Any]型別字典
有了上述的CodingKeys
的分析,就有了能解析出Dictionary<String, Any>
的希望。先總結一下目前已有的基礎:
1. Codable
不是完全不能解析Dictionary<String, Any>
型別,而是需要我們手動重寫encode
、decode
方法。
2. 欄位名是能夠通過實現JSONCodingKeys
例項一個物件來表示動態key
值的,譬如JSONCodingKeys.init("int")
。
3. 結合2中的JSONCodingKeys
,在_JSONKeyedDecodingContainer#allKeys
中是可以獲取到當前這一級字典的所有key的。
眾所周知,Json解析可以理解為一個從外向內遞迴的過程,所以結合上述的3點,就能夠在獲取到所有Key
的前提下,逐層遞迴來生成一個Dictionary<String, Any>
,但Codable
沒有提供這個api,需要我們自己動手。
還是用開頭的例子
json
{
"int": 1234,
"string": "測試",
"double": 10.0002232,
"ext": {
"intExt": 456,
"stringExt": "擴充套件",
"doubleExt": 456.001
}
}
在解析到ext
時,我們就可以通過JSONCodingKeys
獲取所有key
,使用到的是_JSONKeyedDecodingContainer#nestedContainer
方法。
swift
func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}
通過_JSONKeyedDecodingContainer#nestedContainer
獲取到的container
,內部就會擁有ext
那一級下的所有key
值,如intExt
、stringExt
、doubleExt
。接著就可以逐個欄位嘗試來確定value
的型別,最終遞迴生成一個
Dictionary<String, Any>
```swift
func decode(_ type: Dictionary
for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
`encode`方法同理,文章後面會貼出詳細的程式碼。最後只需要**重寫資料實體的`encode`和`decode`方法**
swift
/*
{
"int": 1234,
"string": "測試",
"double": 10.0002232,
"ext": {
"intExt": 456,
"stringExt": "擴充套件",
"doubleExt": 456.001
}
}
/
class TestCodable: NSObject, Codable {
let int: Int
let string: String
let double: Double
let ext: [String: Any]
enum Key: String, CodingKey {
case int
case string
case double
case ext
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: Key.self)
try container.encode(int, forKey: .int)
try container.encode(string, forKey: .string)
try container.encode(double, forKey: .double)
try container.encode(ext, forKey: .ext)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
self.int = try container.decode(Int.self, forKey: .int)
self.string = try container.decode(String.self, forKey: .string)
self.double = try container.decode(Double.self, forKey: .double)
self.ext = try container.decode(Dictionary<String, Any>.self, forKey: .ext)
}
} ```
擴充套件Codable
Dictionary<String, Any>
的解析已經完成,其實Array<Any>
的解析亦是同理。這裡把完整的擴充套件程式碼貼出,僅供參考:
```swift
import Foundation
struct JSONCodingKeys: CodingKey { var stringValue: String
init(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
extension KeyedDecodingContainer {
func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
guard contains(key) else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
guard contains(key) else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()
for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
}
extension UnkeyedDecodingContainer {
mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}
mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}
extension KeyedEncodingContainerProtocol where Key == JSONCodingKeys {
mutating func encode(_ value: Dictionary
extension KeyedEncodingContainerProtocol {
mutating func encode(_ value: Dictionary
mutating func encode(_ value: Array<Any>?, forKey key: Key) throws {
if value != nil {
var container = self.nestedUnkeyedContainer(forKey: key)
try container.encode(value!)
}
}
}
extension UnkeyedEncodingContainer {
mutating func encode(_ value: Array
mutating func encode(_ value: Dictionary<String, Any>) throws {
var nestedContainer = self.nestedContainer(keyedBy: JSONCodingKeys.self)
try nestedContainer.encode(value)
}
} ```
GRDB的編解碼衝突
GRDB
物件型資料庫的讀寫,有依賴Codable
的編解碼。假如有一需求:
有一資料庫實體
db
,db
有一個Dictionary<String, Any>
的物件a
,a
在資料庫以TEXT
型別存在。所以a
在入庫前需要被打包成json
串,以字串的形式寫入。同時,db
實體需要支援Json
編解碼。
相當合理的需求吧!但是問題來了,Codable
可以藉助上述的擴充套件程式碼將a
解析成Dictionary<String, Any>
,但是由於GRDB
重寫的Decoder
協議以及KeyedEncodingContainerProtocol
,nestedContainer
方法是沒有實現的,會導致崩潰
swift
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
fatalError("not implemented")
}
具體程式碼在GRDB.swift/FetchableRecord+Decodable.swift
GRDB
是如何實現Codable
的解析呼叫的呢?這裡以查詢為例,GRDB
的資料庫實體需要實現FetchableRecord
協議,這個協議在原始碼中,若實體同時繼承了Decodable
協議就會有一預設實現:
swift
extension FetchableRecord where Self: Decodable {
public init(row: Row) {
// Intended force-try. FetchableRecord is designed for records that
// reliably decode from rows.
self = try! RowDecoder().decode(from: row)
}
}
該方法最後會呼叫到實體的decode
方法進行例項化。所以解決問題的關鍵就是隻能將重寫該init(row: Row)
方法避免與Json解析衝突,這裡引用官方文件的例子:
```swift
struct Link: FetchableRecord {
var url: URL
var isVerified: Bool
init(row: Row) {
url = row["url"]
isVerified = row["verified"]
}
} ```
最後
本文主要歸納了筆者在開發當中使用Codable
解析Dictionary<String, Any>
的踩坑,利用這一技巧在使用Codable
是也能靈活的解析一些異構的場景。當然如果考慮使用其他Json解析庫的話,alibaba/HandyJSON可能也是不錯的選擇。
如果有興趣的話也可以看看筆者的另一篇關於Codable
的文章:關於Codable協議處理資料實體屬性預設值問題
參考: http://gist.github.com/loudmouth/332e8d89d8de2c1eaf81875cfcd22e24
- 用華為CameraKit實現預覽和拍照
- 重溫今日頭條螢幕適配方案
- 【基於Flutter&Flame 的飛機大戰開發筆記】展示面板及重新開始選單
- 【基於Flutter&Flame 的飛機大戰開發筆記】利用bloc管理遊戲狀態
- 【基於Flutter&Flame 的飛機大戰開發筆記】子彈升級和補給
- 【基於Flutter&Flame 的飛機大戰開發筆記】重構敵機
- 【基於Flutter&Flame 的飛機大戰開發筆記】子彈發射及碰撞檢測
- 【基於Flutter&Flame 的飛機大戰開發筆記】敵機生成器
- 【基於Flutter&Flame 的飛機大戰開發筆記】搭建專案及建立一架戰機
- 一文搞定移動端接入ncnn模型(包括Android、iOS)
- 在Android上實現Metal的計算Demo
- 移動端執行JS指令碼除錯方案-單元測試
- 為什麼Glide4.x中的AppGlideModule不應該出現在Library中
- CameraX OpenGL預覽的全新版本
-
有關Swift Codable解析成Dictionary
的一些事 - 在iOS應用上進行記憶體監控
- 體驗一下用Metal畫圖
- 在iOS上進行WebP編碼是一種怎樣的體驗之為何cpu佔用如此之高?
- 在iOS上進行WebP編碼是一種怎樣的體驗?