SoFunction
Updated on 2025-03-02

Detailed explanation of the usage and principles of swift language Codable

Codable 

Codable itself is a type alias

typealias Codable = Decodable & Encodable

Represents a type that is decoded and encoded in both Decodable and Encodable protocols.

Codable can also represent a codec system developed by Apple for Swift. It was introduced in Swift 4 and includes the Encoder and Decoder protocols and their two implementations.JSONEncoderJSONDecoderandPropertyListEncoderPropertyListDecoder. Among them, Codable and its related protocols are placed in the standard library, while the specific Encoder and Decoder classes are placed in theFoundationIn the framework.

How to use Codable

Codable is used to convert the system's own data structure and external public data structure. The internal data structure of the system can be basic types, structures, enumerations, classes, etc., and the external public data structure can be JSON, XML, etc.

Conversion of JSON and model

When using Objective-C to convert JSON and model, some third-party libraries are generally used. These third-party libraries basically use the powerful features of Objective-C Runtime to realize the interchange of JSON and model.

But Swift is a static language and does not have dynamic Runtime like Objective-C. Although in Swift, you can also use the JSON model intertransformation scheme based on OC Runtime by inheriting NSObject. But this is not so good. Swift also gave up the high performance of Swift as a static language, which is equivalent to saying that it has reduced the operating performance of the entire project, which is unbearable.

Fortunately, Apple providesJSONEncoderandJSONDecoderThese two structures are convenient for converting each other between JSON data and custom models. Apple can use some system-private mechanisms to achieve transformation without the need to passOC Runtime

As long as you make your data type compliant with the Codable protocol, you can use the codec provided by the system to code.

struct User: Codable {
    var name: String
    var age: Int
}

The specific codec code is as follows:

Decoding (JSON Data -> Model):

let json = """
    {
        "name": "zhangsan",
        "age": 25
    }
    """.data(using: .utf8)!
let user = JSONDecoder().decode(, from: json)

Coding (Model -> JSON Data):

let data = JSONEncoder().encode(user)

Codable supports data types

Basic data types

You can see in the declaration file of the Swift standard library that the basic types are all passedextensionImplementedCodableprotocol.

For the properties of the underlying type, both JSONEncoder and JSONDecoder can be handled correctly.

Date

JSONEncoderProvideddateEncodingStrategyProperties to specify date encoding policy. sameJSONDecoderProvideddateDecodingStrategyproperty.

Just take itdateDecodingStrategyFor example, it is an enum type. There are several cases for enum types:

case name effect
case deferredToDate The default case
case iso8601 Decode dates according to ios8601 standard
case formatted(DateFormatter) Custom date decoding policy, need to provide a DateFormatter object
case custom((_ decoder: Decoder) throws -> Date) Custom date decoding strategy, you need to provide a closure of Decoder -> Date

Usually the most commonly used is.iso8601, because the backend return date is usually returned in ios8601 format. As long as the date in JSON is a string of ios8601 specification, just set a line of code to allow JSONDecoder to complete the decoding of the date.

struct User: Codable {
    var name: String
    var age: Int
    var birthday: Date
}
let json = """
    {
        "name": "zhangsan",
        "age": 25,
        "birthday": "2022-09-12T10:25:41+00:00"
    }
    """.data(using: .utf8)!
let decoder = JSONDecoder()
 = .iso8601
let user = (, from: json)
// Correctly decoded to Date type

Nested objects

When nesting objects in a custom model, as long as the nested object also complies with the Codable protocol, the entire object can be used normallyJSONEncoderandJSONDecoderCodec.

struct UserInfo: Codable {
    var name: String
    var age: Int
}
struct User: Codable {
    var info: UserInfo
}

enumerate

The enumeration type must be decoded by its RawValue type, and the type of RawValue corresponds to the JSON field type, so that it can be correctly decoded.

Customize CodingKeys

Custom CodingKeys are mainly for two purposes

  • When the data type attribute name is different from the field name in JSON, make a key mapping.
  • Skip the codec process of certain fields by not adding cases for certain fields.
struct User: Codable {
    var name: String
    var age: Int
    var birthday: Date?
    enum CodingKeys: String, CodingKey {
        case name = "userName"
        case age = "userAge"
    }
}

CodingKeys must be an enum of type RawValue and comply withCodingKeyprotocol. The effect of the above code is to map the name and age fields, so that the birthday field is not included in the encoding and decoding process.

The principle of Codable

After understanding the usage of Codable, let’s take a look at the principles of Codable.

Decodable protocol

Since the principles of encoding and decoding are similar but the direction is different, we only explore more decoding processes used.

What should I do if I want an object to support decoding? Of course, it complies with the Decodable protocol. Let's first look at what an object needs to do in compliance with the Decodable protocol.

DecodableThe protocol is defined as follows:

public protocol Decodable {
    init(from decoder: Decoder) throws
}

In other words, just implement an initialization method of passing in Decoder parameters, so we implement User ourselves.

struct User: Decodable {
    var name: String
    var age: Int
    init(from decoder: Decoder) throws {
    }
}

Now let’s see how to get the values ​​of the two properties of User from the Decoder object.

CheckDecoderThe definition of , it is a protocol. There are two attributes:

var codingPath: [CodingKey] { get }
var userInfo: [CodingUserInfoKey : Any] { get }

There are three more methods:

func container<Key>(keyedBy type: ) throws -> KeyedDecodingContainer<Key> where Key : CodingKey
func unkeyedContainer() throws -> UnkeyedDecodingContainer
func singleValueContainer() throws -> SingleValueDecodingContainer

You will find that the return of these three methods is XXXContainer. Literally, it is a container, and the container must contain something.

Container

If you look at the definitions of these Containers, you will find that there are a series of decode... methods to decode various types.

There are three types of Containers:

Container Type effect
SingleValueDecodingContainer It means that only one value is saved in the container
KeyedDecodingContainer It means that the data stored in the container is saved in the form of key-value pairs.
UnkeyedDecodingContainer It means that the data saved in the container has no keys, that is, the data saved is an array

Going back to the User example above, the JSON data is as follows:

{
    "user": "zhangsan",
    "age": 25
}

This kind of data is obviously a key-value pair, so the data needs to be retrieved using KeyedDecodingContainer. KeyedDecodingContainer should be the most commonly used Container.

struct User: Decodable {
    var name: String
    var age: Int
    init(from decoder: Decoder) throws {
        (keyedBy: &lt;#T###&gt;)
    }
}

The parameter needs to pass a type of an object that conforms to the CodingKey protocol, so you must implement the CodingKeys enumeration yourself and pass it into the parameter.

struct User: Decodable {
    var name: String
    var age: Int
    enum CodingKeys: String, CodingKey {
        case name
        case age
    }
    init(from decoder: Decoder) throws {
        let container = (keyedBy: )
    }
}

Then you can get the data from the container and assign it to your own attributes. Since these methods will throw exceptions, they must be addedtry

init(from decoder: Decoder) throws {
    let container = try (keyedBy: )
    name = try (, forKey: .name)
    age = try (, forKey: .age)
}

Similarly, we can also implement encoding. At this time, change the protocol implemented by User toCodable

struct User: Codable {
    var name: String
    var age: Int
    enum CodingKeys: String, CodingKey {
        case name
        case age
    }
    init(from decoder: Decoder) throws {
        let container = try (keyedBy: )
        name = try (, forKey: .name)
        age = try (, forKey: .age)
    }
    func encode(to encoder: Encoder) throws {
        var encoder = (keyedBy: )
        try (name, forKey: .name)
        try (age, forKey: .age)
    }
}

The encoding process is the opposite of decoding, because it is a key-value pair, and you get it from the encoderKeyedEncoderContainer, then call the encode method to encode the data of the attribute into the container, and thenJSONEncoderTo handle the next thing.

Next we are curious about how the data in Container is stored and how the data in Container and JSON are converted to each other.

Core Principle Analysis (Container <--> JSON)

The decoding process of JSONDecoder

fromJSONDecoder().decode(, from: json)This sentence begins to analyze. Open swift-corelibs-foundationJSONDecodersource code.

// 1
var parser = JSONParser(bytes: Array(data))
let json = try ()
// 2
return try JSONDecoderImpl(userInfo: , from: json, codingPath: [], options: )
    .unwrap(as: ) // 3

The implementation of the decode method is mainly based on these three lines of code.

  • First convert data into a typeJSONValuejson object.
  • Then construct a JSONDecoderImpl object
  • Calling the JSONDecoderImpl objectunwrapThe method gets the object to be converted into.

CheckJSONValueThe definition of JSON is defined through enumeration nesting. The specific data is carried in this enumeration type through the associated value.

enum JSONValue: Equatable {
    case string(String)
    case number(String)
    case bool(Bool)
    case null
    case array([JSONValue])
    case object([String: JSONValue])
}

When obtaining KeyedDecodingContainer, the Container object is built through JSONValue.

// Here is the JSONValue type saved in JSONDecoderImplswitch  {
case .object(let dictionary): // The case of JSONValue and .object match, take out the dictionary data    let container = KeyedContainer&lt;Key&gt;(
        impl: self,
        codingPath: codingPath,
        dictionary: dictionary // Pass in dictionary data    )
    return KeyedDecodingContainer(container)

You can see,KeyedDecodingContainerOnly whenIt can only be created correctly when the match is a dictionary. The data is insidelet dictionary: [String: JSONValue]Save in form.

Looking at other codes, you can find:

SingleValueContainerJust save one directlylet value: JSONValueInside.

UnkeyedDecodingContainerIt's a save an arraylet array: [JSONValue]

Therefore, in the Container calldecodeWhen a method obtains data, it is to obtain data from the data saved by itself based on the parameter key and type. This source code is very simple, you will understand it after reading it.

The last step of unwrap method can be seen through the source code, and the final call is the object itself implemented.init(from decoder: Decoder)method

Therefore, it can be concludedJSON -> Model stepsas follows:

  • JSONParser parses the passed binary JSON data into a JSONValue object.
  • Build JSONDecoderImpl and save the relevant data inside.
  • Call the unwrap method of JSONDecoderImpl and start calling the object implementationinit(from: decoder: Decoder)method
  • In the ``init(from: decoder: Decoder)` method, first obtain the corresponding Container according to the data type.
  • Call the decodeXXX method of Container to get the specific value assigned to the attribute.

Model -> JSON's steps are similar, but the direction is reversed. If you are interested, you can check the source code yourself.

In Swift's JSON model conversion method, by observing the open source library on Github, you can find that there are three implementation solutions in total:

  • Objective-C RuntimeThis solution is basically used by a number of libraries developed by OCs, such as YYModel. This solution is very simple to use and has very little code, but it does not conform to Swift.
  • Key MappingFor example, this is the case with ObjectMapper. The disadvantage of this is that each object has to write a lot of mapping code, which is more troublesome.
  • Utilize the underlying memory layout of the objectSwiftyJSON belongs to this kind of method. This method is also very convenient to use, but relying on Apple's private code, if Apple adjusts its internal implementation, it will fail.

Through the above analysis of the Codable principle, it was found that Codable is basically a Key mapping solution, but the compiler helped us automatically synthesize a lot of code to make it very simple to use. Since the compiler will not help third-party libraries synthesize code, Codable instantly kills a number of third-party libraries based on key mapping.

What did the compiler do for us?

We found that as long as your object complies with the Codable protocol, it can be used normallyJSONEncoderandJSONDecoderCodec does not require the implementation of the methods defined in the protocol.

That's because the compiler generated it for us. This kind of compiler synthesis code is used in many places, such as automatically synthesis of codes that implement Equatable and Hashable for structures and enumerations, codes that implement CaseIterable for enumeration synthesis, etc.

In the above User example, the code compiled for us is as follows:

struct User: Codable {
    var name: String
    var age: Int
    // Compiler synthesis    enum CodingKeys: String, CodingKey {
        case name
        case age
    }
    // Compiler synthesis    init(from decoder: Decoder) throws {
        let container = try (keyedBy: )
        name = try (, forKey: .name)
        age = try (, forKey: .age)
    }
    // Compiler synthesis    func encode(to encoder: Encoder) throws {
        var container = (keyedBy: )
        try (name, forKey: .name)
        try (age, forKey: .age)
    }
}

As you can see, the compiler automatically synthesizes the definition of the CodingKeys enumeration and synthesizes the code that implements the Encodable and Decodable protocols. This provides convenience to developers.

Default value issue

There is a problem with the codec implementation automatically generated by the compiler, which is that it does not support the default value. If you need to support the default value, you need to use it yourselfdecodeIfPresentTo achieve:

struct User: Decodable {
    var name: String
    var age: Int
    enum CodingKeys: String, CodingKey {
        case name
        case age
    }
    init(from decoder: Decoder) throws {
        let container = try (keyedBy: )
        name = try (, forKey: .name) ?? ""
        age = try (, forKey: .age) ?? 0
    }
}

But in this way, each structure has to be implemented once by itself, which is very troublesome. Actually, there are already many articles on this website, just use it@propertyWrapperProperty wrappers to solve this problem.

Property Wrapper @propertyWrapper

The property wrapper is used to wrap a layer between the attribute and the structure that defines the attribute, and is used to implement some common setter and getter logic or initialization logic, etc.

For example, for Int type, the property wrapper can be defined as follows.

@propertyWrapper
public struct DefaultInt: Codable {
    public var wrappedValue: Int
    public init(from decoder: Decoder) throws {
        let container = try ()
        wrappedValue = (try? ()) ?? 0
    }
    public func encode(to encoder: Encoder) throws {
        try (to: encoder)
    }
}

The above code has been implementedinit(from decoder: Decoder)Method to provide a default value of 0 for attributes when decoding fails. accomplishencode(to encoder: Encoder)It is to directly encode the internal value when encoding rather than encode the entire attribute wrapper type.

Many other basic types have the same logic. In order to avoid duplicate code, the paradigm can be used to implement it uniformly.

public protocol HasDefaultValue {
    static var defaultValue: Self { get set }
}
@propertyWrapper
public struct DefaultBaseType&lt;BaseType: Codable &amp; HasDefaultValue&gt;: Codable {
    public var wrappedValue: BaseType
    public init(from decoder: Decoder) throws {
        let container = try ()
        wrappedValue = (try? ()) ?? 
    }
    public func encode(to encoder: Encoder) throws {
        try (to: encoder)
    }
}

Then consider using type alias to define the attribute wrapping keywords for each type. Because if included<or.Waiting for characters will be more troublesome to write.

typealias DefaultInt = DefaultBaseType<Int>
typealias DefaultString = DefaultBaseType<String>

But some types need to be implemented specially.

enumerate

Enumeration types can be converted by rawValue.

@propertyWrapper
public struct DefaultIntEnum<Value: RawRepresentable & HasDefaultEnumValue>: Codable where  == Int {
    private var intValue = 
    public var wrappedValue: Value {
        get { Value(rawValue: intValue)! }
        set { intValue =  }
    }
    public init() {
    }
    public init(from decoder: Decoder) throws {
        let container = try ()
        intValue = (try? ()) ?? 
    }
    public func encode(to encoder: Encoder) throws {
        try (to: encoder)
    }
}

Array

Since the array needs to get data through the UnkeyedDecodingContainer, it needs to be processed separately.

@propertyWrapper
public struct DefaultArray<Value: Codable>: Codable {
    public var wrappedValue: [Value]
    public init() {
        wrappedValue = []
    }
    public init(wrappedValue: [Value]) {
         = wrappedValue
    }
    public init(from decoder: Decoder) throws {
        var container = try ()
        var results = [Value]()
        while ! {
            let value = try ()
            (value)
        }
        wrappedValue = results
    }
    public func encode(to encoder: Encoder) throws {
        try (to: encoder)
    }
}

Object

Because the structure of objects is different, a default value cannot be given. Therefore, aEmptyInitializableThere is only one parameterless initialization method in the protocol.

public protocol EmptyInitializable {
    init()
}

This protocol can be implemented by objects that need to provide default values. However, it needs to be weighed here. If there are relatively high requirements for memory space, using optional values ​​may be a better solution, because an empty object occupies the same amount of space as an object with data.

Use of property wrappers

After using the attribute wrapper to encapsulate each type, just use it like this. When decode, if there is no corresponding field data attribute, it will be initialized to the default value.

struct User {
    @DefaultString var name: String
    @DefaultInt var age: Int
}

I simply encapsulate a library, and our new Swift project is currently in use, the full code is here:/liuduoios/C…

References:

《the swift programming language》

《Advanced Swift》

swift-corelibs-foundation source code

The above is the detailed explanation of the usage and principles of Swift Codable. For more information on the usage and principles of Swift Codable, please pay attention to my other related articles!