有关Swift Codable解析成Dictionary<String, Any>的一些事

10,592 阅读6分钟

前言

假设有一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)
    }
}

Codableencodedecode都会通过实体的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>encodedecode方法

源码分析

JSONEncoder、JSONDecoder源码:

JSONEncoder.swift

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.intKey.string等。
  • 假如我们定义的Key是一个正常的structclass,那就意味着字典中的全部key都会转换成这个类型的对象,如:JSONCodingKeys.init("int")JSONCodingKeys.init("string")等。

解析[String: Any]类型字典

有了上述的CodingKeys的分析,就有了能解析出Dictionary<String, Any>的希望。先总结一下目前已有的基础:

  1. Codable不是完全不能解析Dictionary<String, Any>类型,而是需要我们手动重写encodedecode方法
  2. 字段名是能够通过实现JSONCodingKeys实例一个对象来表示动态key的,譬如JSONCodingKeys.init("int")
  3. 结合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值,如intExtstringExtdoubleExt。接着就可以逐个字段尝试来确定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方法同理,文章后面会贴出详细的代码。最后只需要重写数据实体的encodedecode方法

/**
 {
     "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.swift

GRDB对象型数据库的读写,有依赖Codable的编解码。假如有一需求:

有一数据库实体dbdb有一个Dictionary<String, Any>的对象aa在数据库以TEXT类型存在。所以a在入库前需要被打包成json串,以字符串的形式写入。同时,db实体需要支持Json编解码。

相当合理的需求吧!但是问题来了,Codable可以借助上述的扩展代码将a解析成Dictionary<String, Any>,但是由于GRDB重写的Decoder协议以及KeyedEncodingContainerProtocolnestedContainer方法是没有实现的,会导致崩溃

func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
    fatalError("not implemented")
}

具体代码在GRDB.swift/FetchableRe…

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协议处理数据实体属性缺省值问题

参考: gist.github.com/loudmouth/3…