前言
假设有一json:
{
"int": 1234,
"string": "测试",
"double": 10.0002232,
"ext": {
"intExt": 456,
"stringExt": "扩展",
"doubleExt": 456.001
}
}
日常开发中,常见的会将数据实体确定好,然后再解析:
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>字典类型
struct TestCodable: Codable {
let int: Int
let string: String
let double: Double
let ext: [String: Any]
}
遗憾的是,由于Dictionary<String, Any>类型在Codable是没有默认解析的逻辑,或者说Any这个不确定类型无法直接用Codable解析,所以上述的代码其实是会报错的。但这种做法,对于一些拥有灵活配置的需求来说,又是非常的合理。因此,本文就重点结合这一合理的需求聊聊如何采用Codable库来解析Dictionary<String, Any>,还有在研究过程中发现它与GRDB的不兼容性以及处理方案。
Codable解析
使用
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串的过程。
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>的话会报以下错误
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是一个例外。
public mutating func encode(_ value: Int, forKey key: KeyedEncodingContainer<K>.Key) throws
public mutating func encode<T>(_ value: T, forKey key: KeyedEncodingContainer<K>.Key) throws where T : Encodable
public func decode(_ type: Int.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Int
public func decode<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T where T : Decodable
所以解决问题的核心就是需要一个支持Dictionary<String, Any>的encode和decode方法。
源码分析
JSONEncoder、JSONDecoder源码:
CodingKey
当我们调用JSONDecoder#decode方法时,内部其实是利用JSONSerialization将Json转换为字典的。
// JSONDecoder
open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
let topLevel: Any
do {
topLevel = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
}
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当中。
private struct _JSONKeyedDecodingContainer<K : CodingKey> : KeyedDecodingContainerProtocol {
typealias Key = K
// 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型解析为例:
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类型。
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的普通类型:
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
// _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>的希望。先总结一下目前已有的基础:
Codable不是完全不能解析Dictionary<String, Any>类型,而是需要我们手动重写encode、decode方法。- 字段名是能够通过实现
JSONCodingKeys实例一个对象来表示动态key值的,譬如JSONCodingKeys.init("int")。 - 结合2中的
JSONCodingKeys,在_JSONKeyedDecodingContainer#allKeys中是可以获取到当前这一级字典的所有key的。
众所周知,Json解析可以理解为一个从外向内递归的过程,所以结合上述的3点,就能够在获取到所有Key的前提下,逐层递归来生成一个Dictionary<String, Any>,但Codable没有提供这个api,需要我们自己动手。
还是用开头的例子
{
"int": 1234,
"string": "测试",
"double": 10.0002232,
"ext": {
"intExt": 456,
"stringExt": "扩展",
"doubleExt": 456.001
}
}
在解析到ext时,我们就可以通过JSONCodingKeys获取所有key,使用到的是_JSONKeyedDecodingContainer#nestedContainer方法。
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>
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
}
encode方法同理,文章后面会贴出详细的代码。最后只需要重写数据实体的encode和decode方法
/**
{
"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>的解析亦是同理。这里把完整的扩展代码贴出,仅供参考:
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<String, Any>) throws {
try value.forEach({ (key, value) in
let key = JSONCodingKeys(stringValue: key)
switch value {
case let value as Bool:
try encode(value, forKey: key)
case let value as Int:
try encode(value, forKey: key)
case let value as String:
try encode(value, forKey: key)
case let value as Double:
try encode(value, forKey: key)
case let value as Dictionary<String, Any>:
try encode(value, forKey: key)
case let value as Array<Any>:
try encode(value, forKey: key)
case Optional<Any>.none:
try encodeNil(forKey: key)
default:
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath + [key], debugDescription: "Invalid JSON value"))
}
})
}
}
extension KeyedEncodingContainerProtocol {
mutating func encode(_ value: Dictionary<String, Any>?, forKey key: Key) throws {
if value != nil {
var container = self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
try container.encode(value!)
}
}
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<Any>) throws {
try value.enumerated().forEach({ (index, value) in
switch value {
case let value as Bool:
try encode(value)
case let value as Int:
try encode(value)
case let value as String:
try encode(value)
case let value as Double:
try encode(value)
case let value as Dictionary<String, Any>:
try encode(value)
case let value as Array<Any>:
try encode(value)
case Optional<Any>.none:
try encodeNil()
default:
let keys = JSONCodingKeys(intValue: index).map({ [$0] }) ?? []
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: codingPath + keys, debugDescription: "Invalid JSON value"))
}
})
}
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方法是没有实现的,会导致崩溃
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
fatalError("not implemented")
}
GRDB是如何实现Codable的解析调用的呢?这里以查询为例,GRDB的数据库实体需要实现FetchableRecord协议,这个协议在源码中,若实体同时继承了Decodable协议就会有一默认实现:
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解析冲突,这里引用官方文档的例子:
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协议处理数据实体属性缺省值问题