preface

Suppose we have a json:

{
     "int": 1234."string": "Test"."double": 10.0002232."ext": {
         "intExt": 456."stringExt": "Extension"."doubleExt": 456.001}}Copy the code

In daily development, it is common to identify and parse data entities:

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
}
Copy the code

But like the ext named in the JSON string, this is an extended property, meaning that the requirement should be to have some dynamic fields in that field. In this case, we’d rather parse it to a Dictionary

Dictionary type
,>

struct TestCodable: Codable {
    let int: Int
    let string: String
    let double: Double
    let ext: [String: Any]}Copy the code

Unfortunately, Dictionary

types do not have default resolution in Codable, or Any types cannot be resolved directly in Codable. However, this approach makes perfect sense for some requirements with flexible configuration. In this article, we’ll focus on resolving Dictionary

using Codable libraries and what we learned about GRDB incompatibilities.
,>
,>

Codable parsing

use

let json = "" "{" int ": 1234," string ":" test ", "double" : 10.0002232} "" "
// json -> entity
let jsonData = json.data(using: .utf8)!
let jsonDecoder = JSONDecoder.init(a)let a = try! jsonDecoder.decode(A.self, from: jsonData)

// Entity -> json
let jsonEncoder = JSONEncoder.init(a)let jsonData2 = try! jsonEncoder.encode(testCodable)
let json2 = String.init(data: jsonData2, encoding: .utf8)!
Copy the code

This code is used to parse JSON strings into data entities using a JSONDecoder provided by Codable and package data entities into JSON strings using JSONEncoder.

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)
    }
}
Copy the code

Codable Encode and decode go through entities func Encode (to Encoder: encoder) and init(from decoder: decoder), which the compiler will generate automatically if the developer doesn’t customize it himself.

Why can’t you parse to Any

Going back to the previous example, if we defined Dictionary

, the following error would be reported
,>

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]}Copy the code

Codecs cannot be generated automatically in Codable areas because of the [String: Any] definition. This means Codable can only support its own resolution types, such as integers or types that are inherited from Codable.

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
Copy the code

So the core of the problem is to have an encode and decode method that supports Dictionary

.
,>

Source code analysis

JSONEncoder, JSONDecoder

JSONEncoder.swift

CodingKey

When we call the JSONDecoder#decode method, internally we convert Json to a dictionary using JSONSerialization.

// 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
}
Copy the code

KeyedDecodingContainer, for example, a dictionary will ultimately saved in _JSONKeyedDecodingContainer # of the 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]
Copy the code

Theoretically, all fields in Json can be retrieved from the Container. In decode method, Bool type parsing is used as an example:

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
}
Copy the code

Decode method, through Key# stringValue as key to take out the container within the corresponding value, and the key here is the CodingKey type, combining with the example of the beginning of may be defined as an inherited from CodingKey type, it can be an enum type.

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
}
Copy the code

The advantage of doing this is that you know exactly what fields are in the Json, and you can parse out values based on specific keys to generate entities. However, the limitation is that only the key value can be resolved, which obviously does not meet the requirements of this article. So you can do a bit of a twist and implement a generic type that inherits from 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
    }
}
Copy the code

So it can be _JSONKeyedDecodingContainer # key are represented in the container.

Gets all keys in the dictionary

// _JSONKeyedDecodingContainer
public var allKeys: [Key] {
    return self.container.keys.compactMap { Key(stringValue: $0)}}Copy the code

AllKeys can be obtained from the _JSONKeyedDecodingContainer object to internal through Json parsing to key values in the dictionary, the premise is to support the key type. mean

  • Let’s say we define thetaKeyIs aenumType, thenThe returned collection can only be included in theenumValues defined in, such as:Key.int,Key.stringAnd so on.
  • Let’s say we define thetaKeyIs a normal onestructorclassThat meansAll in the dictionarykeyWill be converted to an object of this type, such as:JSONCodingKeys.init("int"),JSONCodingKeys.init("string")And so on.

Parse the [String: Any] type dictionary

With the CodingKeys analysis above, there is hope that you can parse Dictionary

. First, summarize the existing foundation:
,>

  1. CodableIt’s not completely irresolvableDictionary<String, Any>Type, but need usManual overrideencode,decodemethods.
  2. The field name is yesBy implementingJSONCodingKeysInstantiate an object to represent dynamicskeyvalue, such asJSONCodingKeys.init("int").
  3. In combination with 2JSONCodingKeysIn the_JSONKeyedDecodingContainer#allKeysIs in theTo retrieve all keys of the current level of the dictionary.

Json parsing is known to recurse from outside to inside, so it’s possible to recurse a Dictionary

layer by layer for each Key that is available in Codable. This API is not available in Codable, so you need to do it yourself.
,>

Let’s go back to the example we started with

{
     "int": 1234."string": "Test"."double": 10.0002232."ext": {
         "intExt": 456."stringExt": "Extension"."doubleExt": 456.001}}Copy the code

When parsing to ext, we can get all through JSONCodingKeys key, is used in _JSONKeyedDecodingContainer # nestedContainer method.

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)
}
Copy the code

Through _JSONKeyedDecodingContainer # nestedContainer access to the container, internal will have ext under that level of all key values, such as intExt, stringExt, doubleExt. You can then try to determine the type of value field by field, and eventually recursively generate a Dictionary
,>

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
}
Copy the code

Similarly, the encode method will be detailed later in the article. In the end, you just need to rewrite the encode and decode methods of the data entities

/ * * {" int ", 1234, "string" : "test", "double" : 10.0002232, "ext" : {" intExt ": 456," stringExt ":" extension ", "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)
    }
}
Copy the code

Extension Codable

Dictionary

parsing is complete, as is Array

parsing. The complete extension code is posted here for your reference only:

,>

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)
    }
}
Copy the code

GRDB codec conflict

GRDB.swift

Read and write to GRDB object databases, which depend on Codable codec. Suppose there is a need:

There is a database entity db that has a Dictionary

object A, which exists in the database as TEXT. Therefore, A needs to be packaged into A JSON string and written as a string before entering the library. Also, db entities need to support Json codec.
,>

Quite reasonable demand! But the problem is coming, Codable can use the above code will be parsed into a Dictionary < String, Any >, but due to the Decoder agreement and KeyedEncodingContainerProtocol GRDB rewrite, The nestedContainer method is not implemented and will crash

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

Grdb. swift/FetchableRe…

How does GRDB implement resolution calls to Codable? FetchableRecord protocol is required for GRDB database entities to implement the FetchableRecord protocol. In the source code of this protocol, if entities inherit Decodable protocol, there will be a default implementation:

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)
    }
}
Copy the code

The method is finally instantiated by calling the entity’s decode method. The key is to override the init(row: row) method to avoid conflicts with Json parsing. Here’s an example from the official documentation:

struct Link: FetchableRecord {
    var url: URL
    var isVerified: Bool
    
    init(row: Row) {
        url = row["url"]
        isVerified = row["verified"]}}Copy the code

The last

In this article, WE use this technique to parse Dictionary

in Codable areas. In Codable areas, we can parse Dictionary

in groups. Of course, if you consider using other Json parsing libraries, Alibaba /HandyJSON may also be a good choice.
,>
,>

If you’re interested, check out another article I wrote on Codable: the Codable protocol handles default values for data entity attributes

Reference: gist.github.com/loudmouth/3…