Encoding and decoding

A type indicates that it can be serialized and/or deserialized by declaring that it complies with Encodable and/or Decodable protocols. Both protocols constrain only one method: Encodable restricts encoda(to:), which defines how a type encodes itself; Decodable, on the other hand, constrains an initialization method to create an instance from serialized data:

/// A type can encode itself into some external representation.
public protocol Encodable {
  /// Encode the value into the given encoder.
  public func encode(to encoder: Encoder) throws
}

/// A type can decode itself from some external representation.
public protocol Decodable {
  /// Create a new instance by decoding from the given decoder.
  public init(from decoder: Decoder) throws
}
Copy the code
public typealias Codable = Decodable & Encodable
Copy the code

A minimal example

Automatic compliance protocol

The following Coordinate stores a GPS position information:

struct Coordinate: Codable {
  var latitude: Double
  var longitude: Double
  // No implementation is required
}
Copy the code
struct Placemark: Codable {
  var name: String
  var coordinate: Coordinate
}
Copy the code

Encoding

Swift comes with two encoders, JSONEncoder and PropertyListEncoder (which are defined in Foundation, not in the standard library).

let places = [
  Placemark(name: "Berlin", coordinate: 
            Coordinate(latitude: 52, longitude: 13))
  Placemark(name: "Cape Town", coordinate:
           Coordinate(latitude: -34, longitude: 18)))do {
  let encoder = JSONEncoder(a)let jsonData = try encoder.encode(places) // 129 bytes
  let jsonString = String(decoding: jsonData, as: UTF8.self)
/* [{"name":"Berlin", "coordinate":{"longitude":13,"latitude":52}}, {"name":"Cape Town", "coordinate":{"longitude":18,"latitude":-34}}] */
} catch {
  print(error.localizedDescription)
}
Copy the code

Decoding

The decoder version of JSONEncoder is JSONDecoder.

do {
  let decoder = JSONDecoder(a)let decoded = try decoder.decode([Placemark].self, from: jsonData)
  / / / Berlin (lat: 52.0, lon: 13.0), and Cap Town (lat: 34.0, lon: 18.0)]
  type(of: decoded) // Array<Placemark>
  decoded = = places // true
} catch {
  print(error.localizedDescription)
}
Copy the code

The encoding process

The container

Looking at the Encoder protocol, this is the interface that the Encoder exposes to the encoded value:

/// A type that can encode a value into some external representation.
public protocol Encoder {
  /// Code the coding key path to the current position
  var codingPath: [CodingKey] { get }
  /// The context information set by the user for the encoding.
  var userInfo: [CodingUserInfoKey: Any] { get }
  /// Returns a container that holds multiple values indexed by the given key.
  func container<Key: CodingKey> (keyedBy type: Key.Type)
  	-> KeyedEncodingContainer<Key>
  /// Returns a container for multiple values without a key index.
  func unkeyedContainer(a) -> UnkeyedEncodingContainer
  /// Returns an encoding container suitable for holding a single value.
  func singleValueContainer(a) -> SingleValueEncodingContainer
}
Copy the code

Ignoring codingPath and userInfo, it’s clear that the core function of an Encoder is to provide an encoding Container. A container is a sandbox view stored inside the encoder. By creating a new container for each value to be encoded, the encoder can ensure that each value does not overwrite each other’s data.

There are three types of containers:

  • Keyed Containers encode key-value pairs. Think of the key container as a special dictionary, and this is by far the most commonly used container.
  • An Unkeyed Container is used to encode a sequence of values without the corresponding key. You can think of it as an array of encoded results.
  • Single-value containers encode single values.

For each of these three containers, there is a protocol that constrains how the container should accept and encode a value. The following is the definition of SingleValueEncodingContainer:

/// Supports containers that store and directly encode unindexed single values.
public protocol SingleValueEncodingContainer {
  /// Encode the encoding key path to the current location.
  var codingPath: [CodingKey] { get }
  
  /// Encode null values.
  mutating func encodeNil(a) throws
  
  /// The method to encode the primitive type
  mutating func encode(_ value: Bool) throws
  mutating func encode(_ value: Int) throws
  mutating func encode(_ value: Int8) throws
  mutating func encode(_ value: Int16) throws
  mutating func encode(_ value: Int32) throws
  mutating func encode(_ value: Int64) throws
  mutating func encode(_ value: UInt) throws
  mutating func encode(_ value: UInt8) throws
  mutating func encode(_ value: UInt16) throws
  mutating func encode(_ value: UInt32) throws
  mutating func encode(_ value: UInt64) throws
  mutating func encode(_ value: Float) throws
  mutating func encode(_ value: Double) throws
  mutating func encode(_ value: String) throws
  
  mutating func encode(T: Encodable)(_ value: T) throws
}
Copy the code

UnkeyedEncodingContainer and KeyedEncodingContainerProtocol has the same structure and SingleValueEncodingContainer, but they have more ability, For example, you can create nested containers. If you want to create encoders or decoders for other data formats, the most important part is to implement these containers.

How does the value encode itself

Going back to the previous example, the top-level type we want to code is Array . A keyless container is a great place to store the encoded results of an array (because an array is simply a sequence of values). Therefore, the array will ask the encoder for a keyless container. It then iterates over its own elements and tells the container to code them one by one.

extension Array: Encodable where Element: Encodable {
  public func encode(to encoder: Encoder) throws {
    var container = encoder.unkeyedContainer()
    for element in self {
      try container.encode(element)
    }
  }
}
Copy the code

Composite code

Coding Keys

First, in the Placemark, the compiler generates a private enumeration type called CodingKeys:

struct Placemark {
  // ...
  private enum CodingKeys: CodingKey {
    case name
    case coordinate
  }
}
Copy the code

This enumeration contains members that correspond one to one to the storage properties in the structure. The enumeration value is the key used when the key container encodes the object. These strongly typed keys are safer and more convenient than string keys because the compiler checks for spelling errors. In the end, however, the encoder must be able to convert these keys to strings or integer values for storage purposes. And what does that is the CodingKey protocol:

/// This type is used as the key for encoding and decoding
public protocol CodingKey {
  /// String value in a named set (e.g. dictionary with string as key).
  var stringValue: String { get }
  /// Values used in a collection of integer indexes (a dictionary with integer keys).
  varIntValue:Int? { get }
  init?(stringValue: String)
  init?(intValue: Int)
}
Copy the code

Encode (to) method

Here is an encode(to:) method generated by the compiler for the Placemark structure:

struct Placemark: Codable {
  // ...
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    try container.encode(coordinate, forKey: .coordinate)
  }
}
Copy the code

The main difference with encoding the Placemark array is that the Placemark encodes itself into a key container. For complex data types that have multiple attributes (such as structures and classes), using key containers is the right choice (one exception is Range, which uses a keyless container to encode upper and lower bounds).

The end result of the coding process is a tree of nested containers. The JSON encoder converts this result to the target format based on the type of node in the tree: the key container becomes a JSON object ({… }), the keyless container becomes a JSON array ([]), and the single-valued container is converted to a number, Boolean, string, or null, depending on their data type.

Init (from:) indicates the initialization method

When we call try decoder.decode([Placemark].self, from: jsonDate), the decoder creates an instance of the type we pass in, using the initialization methods defined in Decodable. Like an encoder, a decoder manages a tree made of decoding containers, which we are already familiar with: key containers, keyless containers, and single-valued containers.

Each decoded value is recursively accessed down the container hierarchy, and the corresponding property is initialized with the value decoded from the container. If an error occurs in one of the steps (such as because of a type mismatch or a value that does not exist), the entire process fails and an error is thrown.

struct Placemark: Codable {
  // ...
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: Codingkeys.self)
    name = try container.decode(String.self, forKey: .name)
    coordinate = try container.decode(Coordinate.self, forKey: .coordinate)
  }
}
Copy the code

Manual compliance

Custom Coding Keys

The easiest way to control how a type codes itself is to create a custom CodingKeys enumeration for it. It allows us, in a very simple, declarative way, to change the way the type is encoded. In this enumeration, we can:

  • In the encoded output, rename the field with an explicitly specified string value.
  • Removes a key from the enumeration to skip its corresponding field.

To set a different name, we need to explicitly set the underlying type of the enumeration to String.

struct Placemark2: Codable {
  var name: String
  var coordinate: Coordinate
  
  private enum CodingKeys: String.CodingKey {
    case name = "label"
    case coordinate
  }
  
  // Encode and decode methods synthesized by the compiler will use the overridden CodinhKeys.
}
Copy the code

In the following implementation, the enumeration does not contain the name key, so the name of the map marker will be skipped and only the GPS coordinates will be encoded:

struct Placemark3: Codable {
  var name: String = "(Unknown)"
  var coordinate: Coordinate
  
  private enum CodingKeys: CodingKey {
    case coordinate
  }
}
Copy the code

Custom encode(to:) and init(from:) implementations

struct Placemark4: Codable {
  var name: String
  var coordinate: Coordinate?
}
Copy the code

Suppose a JSON like this:

let invalidJSONInput = """ [ { "name": "Berlin", "coordinate": {} } ] """
Copy the code

Because latitude and Longitude fields do not exist in coordinate, a.keynotFound error is triggered:

do {
  let inputData = invalidJSONInput.data(using: .utf8)!
  let decoder = JSONDecoder(a)let decoded = try decoder.decode([Placemark4].self, from: inputData)
} catch {
  print(error.localizedDescription)
  // The data couldn't be read because it is missing.
}
Copy the code

We can override Decodable’s initialization method to explicitly catch the errors we expect:

struct Placemark4: Codable {
  var name: String
  var coordinate: Coordinate?
  
  // encode(to:) is still synthesized by the compiler
  
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decode(String.self, forKey: .name)
    do {
      self.coordinate = try container.decodeIfPresent(Coordinate.self, 
                                                      forKey: .coordinate)
    } catch DecodingError.keyNotFound {
   			self.coordinate = nil}}}Copy the code

Now, the decoder can successfully decode the wrong JSON:

do {
  let inputData = invalidJSONInput.data(using: .utf8)!
  let decoder = JSONDecoder(a)let decoded = try decoder.decode([Placemark4].self, from: inputData)
  decoded // [Berlin(nil)]
} catch {
  print(error.localizedDescription)
}
Copy the code

Common coding tasks

Make everyone else’s code Codable

import CoreLocation

struct Placemark5: Codable {
  var name: String
  var coordinate: CLLocationCoordinate2D
} 
// Error: Unable to automatically compose 'Decodable'/'Encodable' adaptation code,
// Because 'CLLocationCoordinate2D' does not follow the protocol
Copy the code
extension CLLocationCoordinate2D: Codable {}
// Error: 'Encodable' code cannot be implemented outside the type-defined file by extending automatic synthesis.
Copy the code
extension Placemark5 {
  private enum CodingKeys: String.CodingKey {
    case name
    case latitude = "lat"
    case longitude = "lon"
  }
  
  func encode(to encoder: Encoder) throws {
    var container = encoder.contaienr(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    // Code latitude and longitude separately
    try container.encode(coordinate.latitude, forKey: .latitude)
    try container.encode(coordinate.longitude, forKey: .longitude)
  }
  
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decode(String.self, forKey: .name)
    // Rebuild CLLocationCoordinate2D from latitude and longitude
    self.coordinate = CLLocationCoordinate2D(
      latitude: try container.decode(Double.self, forKey: .latitude),
    	longitude: try container.decode(Double.self, forkey: .longitude)
    )
  }
}
Copy the code

Another option is to use nested containers to encode latitude and longitude. KeyedDecodingContainer has a method called nestedContainer(keyedBy:forKey:), which creates a new nested key container on the key specified by forKey. This nested key container uses another set of encoding keys specified by the keyedBy argument. So we just define an enumeration that implements CodingKeys, uses it as a key, and encodes latitude and longitude in a nested key container:

struct Placemark6: Encodable {
  var name: String
  var coordinate: CLLocationCoordinate2D
  
  private enum CodingKeys: CodingKey {
    case name
    case coordinate
  }
  
  // The encoding key for the nested container
  private enum CoordinateCodingKeys: CodingKey {
    case latitude
    case longitude
  }
  
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(name, forKey: .name)
    var coordinateContainer = container.nestedContainer(
    	keyedBy: CoordinatedCodingKeys.self, forKey: .coordinate)
    try coordinateContainer.encode(coordinate.latitude, forKey: .latitude)
    try coordinateContainer.encode(coordinate.longitude, forKey: .longitude)
  }
}
Copy the code

Thus, we effectively reconstructed the Coordinate type encoding in the Placemark result body without exposing the embedded type to the Codable system.

Another strategy, in the PlaceMark, we define a private property of type Coordinate, called Coordinate, to store position information. The user is then exposed to a calculated property coordinate of the CLLocationCoordinate2D type.

struct Placemark7: Codable {
  var name: String
  private var _coordinate: Coordinate
  var coordinate: CLLocationCoordinate2D {
    get {
      return CLLocationCoordinate2D(latitude: _coordinate.latitude,
                                   longitude: _coordinate.longitude)
    }
    set {
      _coordinate = Coordinate(latitude: newValue.latitude,
                              longitude: newValue.longitude)
    }
  }
  
  private enum CodingKeys: String.CodingKey {
    case name
    case _coordinate = "coordinate"}}Copy the code

Make the class Codable

We cannot append the Codable feature to a non-final class as an extension. The recommended approach is to write a structure to encapsulate the class and encode the structure.

Make enumerations Codable

The compiler can also automatically synthesize Codable code for enumerations that implement the RawRepresentable protocol, as long as the enumerated RawValue type is native to those categories: Bool, String, Float, Double, and various forms of integer. For other cases, such as enumerations with associated values, add the Codable implementation manually.

Either is A very common type that expresses an alternative concept. It represents A value that can be Either an object of the generic argument A or an object of the generic argument B:

enum Either<A.B> {
  case left(A)
  case right(B)}Copy the code
extension Either: Codable where A: Codable.B: Codable {
  private enum CodingKeys: CodingKey {
    case left
    case right
  }
  
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    switch self {
      case .left(let value):
      	try container.encode(value, forKey: .left)
      case .right(let value):
      	try container.encode(value, forKey: .right)
    }
  }
  
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    if let leftValue = try container.decodeIfPresent(A.self, forKey: .left) {
      self = .left(leftValue)
    } else {
      let rightValue = try container.decode(B.self, forKey: .right)
      self = .right(rightValue)
    }
  }
}
Copy the code
let values: [Either<String.Int>] = [
  .left("Forty-two"),
  .right(42)]do {
  let encoder = PropertyListEncoder()
  encoder.outputFormat = .xml
  let xmlData = try encoder.encode(values)
  let xmlString = String(decoding: xmlData, as: UTF8.self)
/ * 
       
       < plist Version ="1.0"> 
      
         left
        
          forty-two 
        
          Right 
        
         42
          */
  
  let decoder = PropertyListDecoder(a)let decoded = try decoder.decode([Either<String.Int>].self, from: xmlData)
/* [Either
      
       .left("Forty-two"), Either
       
        .right(42)] */
       ,>
      ,>
} catch {
  print(error.localizedDescription)
}
Copy the code

Decode polymorphic collections

The decoder requires that we pass in a specific type for the value to be decoded. Intuitively, this makes sense: the decoder needs to know the specific type to call the appropriate initialization method, and since the encoded data typically contains no type information, the type must be provided by the caller. The result of this emphasis on strong typing is that there is no possibility of polymorphism in the decoding step.

Let’s say we want to code an array of UIViews like this. (Assuming both UIView and its subclasses are now Codable, which they are not.)

let views: [UIView] = [label, imageView, button]
Copy the code

If we encode this array, and we decode it, all the decoder can get back is the normal UIView object, because all it knows about the decoded data type is [UIView].self.

The best way to encode such a collection of polymorphic objects is to create an enumeration where each case corresponds to the subclass we want to support, and the associated value of the case is the corresponding subclass object:

enum View {
  case view(UIView)
  case label(UILabel)
  case imageView(UIImageView)
  // ...
}
Copy the code