Codable use

The Codable protocol is a post-Swift4.0 Codable protocol that is handy for converting Json to Model, as shown below

// 1
struct LYPerson:Codable {
    var name: String?
    var age: Int?
}
// 2
let dict: [String : Any] = ["name": "Jack", "age": 10]
let decoder = JSONDecoder()
let data = try JSONSerialization.data(withJSONObject: dict, options: .fragmentsAllowed)
let p = try decoder.decode(LYPerson.self, from: data)
Copy the code
  • 1, custom objects, comply withCodableThe agreement.
  • 2. InitializationJSONDecoderObject that serializes json data and then decodes it.

This allows us to map dict key-value pairs to LYPerson objects.

Source code analysis

Decoder

Let’s open swift-Foundation (version 5.3.2) and take a look at codable definitions

public typealias Codable = Decodable & Encodable
Copy the code

Codable is another name for Decodable and Encodable

public protocol Decodable {

    /// Creates a new instance by decoding from the given decoder.
    ///
    /// This initializer throws an error if reading from the decoder fails, or
    /// if the data read is corrupted or otherwise invalid.
    ///
    /// - Parameter decoder: The decoder to read data from.
    init(from decoder: Decoder) throws
}
Copy the code

The Decodable protocol has only one init(from decoder:Decode) method

When we decode:

Initialize a JSONDecoder object.

Decode

(_ type: t.type, from data: data)

Next, let’s look at what the decode method does.

    open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T {
        let topLevel: Any
        do {
            topLevel = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
        } 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

1. The argument passed is the decoded Type, in this case LYPerson. The other parameter is Data Data, or dict.

2. Json serialization of Data.

3. Create an inner class _JSONDecoder and place Json data and decoder policy.

4. Call the unbox method.

Decoding strategy

When we initialize the JSONDecoder object, the default decoder policy is used if no decoder policy is set

   fileprivate var options: _Options {
        return _Options(dateDecodingStrategy:  `dateDecodingStrategy = secondsSince1970`,
                        dataDecodingStrategy: dataDecodingStrategy,
                        nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
                        keyDecodingStrategy: keyDecodingStrategy,
                        userInfo: userInfo)
    }
Copy the code

Let’s take Date as an example

    fileprivate func unbox(_ value: Any, as type: Date.Type) throws -> Date? {
        guard !(value is NSNull) else { return nil }

        switch self.options.dateDecodingStrategy {
        case .deferredToDate:
            self.storage.push(container: value)
            defer { self.storage.popContainer() }
            return try Date(from: self)
		// 1
        case .secondsSince1970:
            let double = try self.unbox(value, as: Double.self)!
            return Date(timeIntervalSince1970: double)

        case .millisecondsSince1970:
            let double = try self.unbox(value, as: Double.self)!
            return Date(timeIntervalSince1970: double / 1000.0)

        case .iso8601:
            if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
                let string = try self.unbox(value, as: String.self)!
                guard let date = _iso8601Formatter.date(from: string) else {
                    throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Expected date string to be ISO8601-formatted."))
                }

                return date
            } else {
                fatalError("ISO8601DateFormatter is unavailable on this platform.")
            }
	// 2
        case .formatted(let formatter):
            let string = try self.unbox(value, as: String.self)!
            guard let date = formatter.date(from: string) else {
                throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Date string does not match format expected by formatter."))
            }

            return date
	// 3
        case .custom(let closure):
            self.storage.push(container: value)
            defer { self.storage.popContainer() }
            return try closure(self)
        }
    }
Copy the code
  • 1. If the time policy issecondsSince1970, the timestamp from 1970 to the present is returned.
  • 2. If there is a definite time format, return the time of the specified format.
  • 3. If it is custom decoding, closure callback is performed.

Through setting different decoding strategies, to complete the personalized decoding.

unbox

Let’s first look at the implementation of the unbox method

    fileprivate func unbox_(_ value: Any, as type: Decodable.Type) throws -> Any? {
        #if DEPLOYMENT_RUNTIME_SWIFT
        
        print(type)
        // Bridging differences require us to split implementations here
        if type == Date.self {
            guard let date = try self.unbox(value, as: Date.self) else { return nil }
            return date
        } else if type == Data.self {
            guard let data = try self.unbox(value, as: Data.self) else { return nil }
            return data
        } else if type == URL.self {
            guard let urlString = try self.unbox(value, as: String.self) else {
                return nil
            }

            guard let url = URL(string: urlString) else {
                throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath,
                                                                        debugDescription: "Invalid URL string."))
            }
            return url
        } else if type == Decimal.self {
            guard let decimal = try self.unbox(value, as: Decimal.self) else { return nil }
            return decimal
        } else if let stringKeyedDictType = type as? _JSONStringDictionaryDecodableMarker.Type {
            return try self.unbox(value, as: stringKeyedDictType)
        } else {
            self.storage.push(container: value)
            defer { self.storage.popContainer() }
            return try type.init(from: self)
        }
Copy the code
  • 1, if yesData.Date.URLType, generate an instance variable of the corresponding type according to different encoding strategies.
fileprivate func unbox<T>(_ value: Any, as type: _JSONStringDictionaryDecodableMarker.Type) throws -> T? { guard ! (value is NSNull) else { return nil } var result = [String : Any]() guard let dict = value as? NSDictionary else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) } let elementType = type.elementType for (key, value) in dict { let key = key as! String self.codingPath.append(_JSONKey(stringValue: key, intValue: nil)) defer { self.codingPath.removeLast() } result[key] = try unbox_(value, as: elementType) } return result as? T } fileprivate protocol _JSONStringDictionaryDecodableMarker { static var elementType: Decodable.Type { get } } extension Dictionary : _JSONStringDictionaryDecodableMarker where Key == String, Value: Decodable { static var elementType: Decodable.Type { return Value.self } }Copy the code
  • 1, if yesDictionaryType, the key-value pair is traversed and the value is decoded.

In this case, type == LYPerson is initially passed in, which calls its init(from decoder: decoder) method. The init(from decoder: decoder) method for the LYPerson object is undeclared, so where does it come from? The editor does something for us.

SIL analysis

We converted the code to SIL

struct LYPerson : Decodable & Encodable { @_hasStorage @_hasInitialValue var name: String? { get set } @_hasStorage @_hasInitialValue var age: Int? { get set } init(name: String? = nil, age: Int? = nil) init() enum CodingKeys : CodingKey { case name case age @_implements(Equatable, ==(_:_:)) static func __derived_enum_equals(_ a: LYPerson.CodingKeys, _ b: LYPerson.CodingKeys) -> Bool var hashValue: Int { get } func hash(into hasher: inout Hasher) var stringValue: String { get } init? (stringValue: String) var intValue: Int? { get } init? (intValue: Int) } init(from decoder: Decoder) throws func encode(to encoder: Encoder) throws }Copy the code

As you can see, the compiler automatically generates CodingKeys and codec methods for us. The initialization methods are as follows

// LYPerson.init(from:) sil hidden @$s4main8LYPersonV4fromACs7Decoder_p_tKcfC : $@convention(method) (@in Decoder, @thin LYPerson.Type) -> (@owned LYPerson, @error Error) { // %0 "decoder" // users: %71, %52, %12, %3 // %1 "$metatype" bb0(%0 : $*Decoder, %1 : $@thin LYPerson.Type): %2 = alloc_stack $LYPerson, var, name "self" // users: %43, %27, %9, %6, %53, %72, %73, %54 debug_value_addr %0 : $*Decoder, let, name "decoder", argno 1 // id: %3 debug_value undef : $Error, var, name "$error", argno 2 // id: %4 %5 = enum $Optional<String>, #Optional.none! enumelt // user: %7 %6 = struct_element_addr %2 : $*LYPerson, #LYPerson.name // user: %7 store %5 to %6 : $*Optional<String> // id: %7 %8 = enum $Optional<Int>, #Optional.none! enumelt // user: %10 %9 = struct_element_addr %2 : $*LYPerson, #LYPerson.age // user: %10 store %8 to %9 : $*Optional<Int> // id: %10 %11 = alloc_stack $KeyedDecodingContainer<LYPerson.CodingKeys>, let, name "container" // users: %48, %47, %40, %68, %67, %24, %62, %61, %16, %57 %12 = open_existential_addr immutable_access %0 : $*Decoder to $*@opened("D0F7E680-7A9A-11EB-8646-186590D5CC1F") Decoder // users: %16, %16, %15 %13 = metatype $@thin LYPerson.CodingKeys.Type %14 = metatype $@thick LYPerson.CodingKeys.Type // user: %16 %15 = witness_method $@opened("D0F7E680-7A9A-11EB-8646-186590D5CC1F") Decoder, #Decoder.container : <Self where Self : Decoder><Key where Key : CodingKey> (Self) -> (Key.Type) throws -> KeyedDecodingContainer<Key>, %12 : $*@opened(" D0F7E680-7A9A-11EB-8646-186590D5CC1f ") Decoder: $@convention(witness_method: Decoder) <τ_0_0 where τ_0_0: Decoder > < tau _1_0 where tau _1_0: CodingKey> (@thick τ _1_0.type, @in_guaranteed τ_0_0) -> (@out KeyedDecodingContainer<τ_1_0>, @error Error) // type-defs: %12; user: %16 try_apply %15<@opened("D0F7E680-7A9A-11EB-8646-186590D5CC1F") Decoder, LYPerson.CodingKeys>(%11, %14, %12) : $@convention(witness_method: Decoder) <τ_0_0 where τ_0_0: Decoder><τ_1_0 where τ_1_0: CodingKey> (@thick τ _1_0.type, @in_guaranteed τ_0_0) -> (@out KeyedDecodingContainer<τ_1_0>, @error error), normal BB1, error bb4 // type-defs: %12; id: %16Copy the code
  • Create a Container:KeyedDecodingContainer
  • 2, then call the Decoder protocol from the protocol witness tablecontainermethods

_JSONDecoder complies with the Decoder protocol

public protocol Decoder {

    /// The path of coding keys taken to get to this point in decoding.
    var codingPath: [CodingKey] { get }

    /// Any contextual information set by the user for decoding.
    var userInfo: [CodingUserInfoKey : Any] { get }

    /// Returns the data stored in this decoder as represented in a container
    /// keyed by the given key type.
    ///
    /// - parameter type: The key type to use for the container.
    /// - returns: A keyed decoding container view into this decoder.
    /// - throws: `DecodingError.typeMismatch` if the encountered stored value is
    ///   not a keyed container.
    func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey

    /// Returns the data stored in this decoder as represented in a container
    /// appropriate for holding values with no keys.
    ///
    /// - returns: An unkeyed container view into this decoder.
    /// - throws: `DecodingError.typeMismatch` if the encountered stored value is
    ///   not an unkeyed container.
    func unkeyedContainer() throws -> UnkeyedDecodingContainer

    /// Returns the data stored in this decoder as represented in a container
    /// appropriate for holding a single primitive value.
    ///
    /// - returns: A single value container view into this decoder.
    /// - throws: `DecodingError.typeMismatch` if the encountered stored value is
    ///   not a single value container.
    func singleValueContainer() throws -> SingleValueDecodingContainer
}
Copy the code

Decoder in the protocol defines three types of containers: KeyedDecodingContainer, UnkeyedDecodingContainer, SingleValueDecodingContainer.

  • KeyedDecodingContainer is like a dictionary, key-value container, and the key-value is strongly typed.
  • UnkeyedDecodingContainer is like an array, a continuous value container with no keys.
  • Container SingleValueDecodingContainer basic data types.

In KeyedDecodingContainer, the basic data types are decoded and assigned

Custom decoding method


struct LYPerson: Codable {
    var name: String?
    var age: Int?
    
    enum CodingKeys : CodingKey {
      case name
      case age
    }
    init(from decoder: Decoder) throws {
        
        let container = try decoder.container(keyedBy: CodingKeys.self)
            
        self.name = try container.decode(String.self, forKey: .name)
        
        self.age = try container.decode(Int.self, forKey: .age)
        
    }
}


let dict: [String : Any] = ["name": "Jack", "age": 10]

let decoder = JSONDecoder()


let data = try JSONSerialization.data(withJSONObject: dict, options: .fragmentsAllowed)
let p = try decoder.decode(LYPerson.self, from: data)

print(p.age)

Copy the code

1. Define CodingKeys and declare the decoded key.

2. Get KeyedDecodingContainer through JSONDecoder.

3. Decode and assign values to basic data types using container objects.