Ken Muse

Creating Swift Binary Decoders


This is a post in the series Building a Workout App for watchOS in Swift. The posts in this series include:

If we’re going to build a Bluetooth application, we need to be able to decode the data we receive. For most cases, we need to merely conform our types to the Decodable protocol. With that, the built-in decoders can handle the rest. Unfortunately, there is no native way to decode binary data serialized from Bluetooth devices. In this case, we need a way to decode the data ourselves. To decode that data, we need to create a type that conforms to Decoder. This post will explore how to do that.

The Decoder protocol is deceptively simple. It has just a few methods and properties to implement. The complexity to implementing the protocol comes from two aspects – the need to return two custom container types and the actual decoding process. Of course, you can also throw for unimplemented container methods.

The basic structure of the decoder is simply:

 1public struct BinaryDecoder: Decoder {
 2  public var codingPath: [CodingKey] { return [] }
 3  
 4  public var userInfo: [CodingUserInfoKey : Any] { return [:] }
 5  
 6  public func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
 7    return KeyedBinaryDecodingContainer(KeyedContainer<Key>(decoder: self))
 8  }
 9  
10  public func unkeyedContainer() throws -> UnkeyedDecodingContainer {
11    return UnkeyedBinaryContainer(decoder: self)
12  }
13  
14  public func singleValueContainer() throws -> SingleValueDecodingContainer {
15    return SingleValueBinaryContainer(decoder: self)
16  }
17}

Since we’re decoding binary data in a specific way, we don’t necessarily need the codingPath or userInfo. We can simply return empty values for those properties. Of course, if you have more complex needs or rely on userInfo, you can implement those as needed. If you think you might need those, it helps to understand what they do.

The userInfo property is a dictionary that can be used to pass information to the decoder. This additional information can be used by the decoder to alter how it (or a Decodable) processes the data. That can include custom configuration details or default values. In our case, we don’t need to pass any information.

The value codingPath indicates the path to the current point in the data structure being decoded. That means a keyed container from the decoder will start with the current key (or at the root). If a new container is requested from the key container, it would need to add that to the coding path. If we wanted to support that, we’d just replace the code with:

 1let codingPath: [CodingKey]
 2
 3let userInfo: [CodingUserInfoKey : Any]
 4
 5public init(userInfo: [CodingUserInfoKey: Any], codingPath: [CodingKey] = []) {
 6  self.codingPath = codingPath
 7  self.userInfo = userInfo
 8}
 9func decoder(`for` key: CodingKey) -> BinaryDecoder {
10  return .init(userInfo: userInfo, codingPath: codingPath + [key])
11}

This would allow child containers to append keys and pass down any custom userInfo.

The Keyed Decoding Container

Containers are simply responsible for creating a “context” for decoding. They alow for a logical grouping of values and provide a set of methods that can be used for decoding. They also enable you to create child contexts – more containers – for processing groups of data within the current context. This makes it easier to work with logically organized data. One of these is the keyed decoding container (defined by KeyedDecodingContainerProtocol).

The keyed decoding container is responsible for making it possible to retrieve and decode specific properties (keys). Because we’re trying to decode binary data in a fixed format, we don’t necessarily need to retrieve keyed values. The contents need to deserialize in a defined order, so we are unlikely to use this container. While the protocol defines a lot of methods, only a handful typically need to be implemented thanks to the compiler-generated stubs. Since we don’t really need it for this purpose, I’ll use a simple implementation and annotate some of the process.

 1private struct KeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
 2  // The parent decoder (more on this below)
 3  var decoder: BinaryDecoder
 4  
 5  // The path to the current key (could also return decoder.codingPath)
 6  var codingPath: [CodingKey] { return [] }
 7  
 8  // They keys available for decoding from this container
 9  var allKeys: [Key] { return [] }
10  
11  // Does the key exist in this container? Returning true to allow this
12  // to decode any key by just reading the next bits from the data stream.
13  func contains(_ key: Key) -> Bool {
14    return true
15  }
16  
17  // Decode an arbitrary type using the built-in logic.
18  func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
19    // Or decoder.decoder(for: key).decode(T.self) if you need
20    // to preserve key paths.
21    return try decoder.decode(T.self)
22  }
23  
24  // Indicates whether the value of a given key is nil
25  func decodeNil(forKey key: Key) throws -> Bool {
26    return false
27  }
28  
29  // Creates a container for a child key
30  func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey   CodingKey {
31    // Or decoder.decoder(for: key).container(keyedBy: type) to append a key
32    return try decoder.container(keyedBy: type)
33  }
34  
35  // Create an unkeyed container for a child key. This allows retrieving its values
36  // without relying on keys
37  func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
38    // Or decoder.decoder(for: key).decode.unkeyedContainer() to append a key
39    return try decoder.unkeyedContainer()
40  }
41  
42  // Returns a decoder for `super`. This is why it's important to pass the decoder
43  // that was used to create the container.
44  func superDecoder() throws -> Decoder {
45    return decoder
46  }
47  
48  // Returns a decoder for `super` from the container associated with the specified key.
49  func superDecoder(forKey key: Key) throws -> Decoder {
50    return decoder
51  }
52}

This is a very simple boilerplate that ignores the key and decodes the next value. It treats every read as a non-nullable value by default. If you don’t need keyed decoding and won’t be deserializing an entity based on its keys, this seems like a lot of work. You might be wondering why you can’t just throw an error and skip this step. It turns out that like many protocols, there’s a hidden behavior. If you attempt to decode a struct using .init(from: decoder), the the generated code will actually rely on a keyed decoding container. The compiler will also generate the CodingKeys for the type. That means any code using that method would fail if we throw an error instead of returning a container.

Unkeyed decoding container

Unkeyed containers provide a way to encapsulate a set of properties without keys. The behaviors are very similar, but most of the methods do not require a key to be provided. In fact, a basic template for the type is simple:

 1private struct UnkeyedBinaryContainer: UnkeyedDecodingContainer {
 2  // The parent decoder
 3  var decoder: BinaryDecoder
 4  
 5  // The coding path (see above for details on coding keys)
 6  var codingPath: [CodingKey] { return [] }
 7  
 8  // The number of elements in the container (or `nil` for unknown)
 9  var count: Int? { return nil }
10  
11  // An index that is incremented as elements are decoded
12  var currentIndex: Int { decoder.currentIndex }
13
14  // A boolean value indicating there are no more elements to decode
15  var isAtEnd: Bool { return decoder.isAtEnd }
16  
17  // Decode an arbitrary type using the built-in logic.
18  func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
19    return try decoder.decode(type)
20  }
21  // Decodes a null value and returns true if the value is null
22  func decodeNil() -> Bool {
23    return false
24  }
25  
26  // Create a nested keyed container
27  func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey: CodingKey {
28    return try decoder.container(keyedBy: type)
29  }
30  
31  // Create a nested, unkeyed container
32  func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
33    return self
34  }
35  
36  // Return the parent decoder
37  func superDecoder() throws -> Decoder {
38      return decoder
39  }

Unlike the previous container type, this container must reveal the current position of the data being read and whether or not there is data remaining. For now, we’ll delegate that work back to the decoder.

Single value decoding container

As the name suggests, the purpose of this container is to decode a single primitive value. Because of that, it has the simplest implementation. In fact, it’s often combined with an unkeyed container into a single struct in many implementations. The basic structure won’t need any further explanation:

 1private struct SingleValueBinaryContainer: SingleValueDecodingContainer {
 2    var decoder: BinaryDecoder
 3    var codingPath: [any CodingKey]
 4    
 5    func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
 6        return try decoder.decode(type)
 7    }
 8
 9    func decodeNil() -> Bool {
10        return false
11    }
12}

There is an interesting characteristic of the single value container. It only allows decoding a single value. That means that I should not be able to use the container to read additional values, even if they are the same data type. It also should not advance the index of the data being decoded more than once.

It’s worth quickly mentioning that Swift will handle simple initializers such as try UInt32.init(from: decoder) by creating a single value container.

What’s missing?

The astute reader may notice that we’re missing a few things. The most obvious is that none of these implementations are actually decoding anything. If you explore tHe protocols we’ve implemented, they actually have numerous decode methods on them that we haven’t implemented or discussed. Instead, we’ve left everything to be handled through the generic decode<T>. While that might handle some types, it doesn’t quite implement the complete set of functionality. The actual decoding methods are the same with SingleValueDecodingContainer and UnkeyedDecodingContainer. For now, we can delegate those back to the decoder:

 1func decode(_ type: Bool.Type) throws -> Bool {
 2  return try decoder.decode(type)
 3}
 4
 5func decode(_ type: String.Type) throws -> String {
 6  return try decoder.decode(type)
 7}
 8
 9func decode(_ type: Double.Type) throws -> Double {
10  return try decoder.decode(type)
11}
12
13func decode(_ type: Float.Type) throws -> Float {
14  return try decoder.decode(type)
15}
16
17func decode(_ type: Int.Type) throws -> Int {
18  return try decoder.decode(type)
19}
20
21func decode(_ type: Int8.Type) throws -> Int8 {
22  return try decoder.decode(type)
23}
24
25func decode(_ type: Int16.Type) throws -> Int16 {
26  return try decoder.decode(type)
27}
28
29func decode(_ type: Int32.Type) throws -> Int32 {
30  return try decoder.decode(type)
31}
32
33func decode(_ type: Int64.Type) throws -> Int64 {
34  return try decoder.decode(type)
35}
36
37func decode(_ type: UInt.Type) throws -> UInt {
38  return try decoder.decode(type)
39}
40
41func decode(_ type: UInt8.Type) throws -> UInt8 {
42  return try decoder.decode(type)
43}
44
45func decode(_ type: UInt16.Type) throws -> UInt16 {
46  return try decoder.decode(type)
47}
48
49func decode(_ type: UInt32.Type) throws -> UInt32 {
50  return try decoder.decode(type)
51}
52
53func decode(_ type: UInt64.Type) throws -> UInt64 {
54  return try decoder.decode(type)
55}

For the KeyedDecodingContainer, the decode methods require a key to be passed in. We can ignore the key for now and just delegate to the decoder:

 1func decode(_ type: Bool.Type, forKey key: Self.Key) throws -> Bool {
 2  return try decoder.decode(type)
 3}
 4
 5func decode(_ type: String.Type, forKey key: Self.Key) throws -> String {
 6  return try decoder.decode(type)
 7}
 8
 9func decode(_ type: Double.Type, forKey key: Self.Key) throws -> Double {
10  return try decoder.decode(type)
11}
12
13func decode(_ type: Float.Type, forKey key: Self.Key) throws -> Float {
14  return try decoder.decode(type)
15}
16
17func decode(_ type: Int.Type, forKey key: Self.Key) throws -> Int {
18  return try decoder.decode(type)
19}
20
21func decode(_ type: Int8.Type, forKey key: Self.Key) throws -> Int8 {
22  return try decoder.decode(type)
23}
24
25func decode(_ type: Int16.Type, forKey key: Self.Key) throws -> Int16 {
26  return try decoder.decode(type)
27}
28
29func decode(_ type: Int32.Type, forKey key: Self.Key) throws -> Int32 {
30  return try decoder.decode(type)
31}
32
33func decode(_ type: Int64.Type, forKey key: Self.Key) throws -> Int64 {
34  return try decoder.decode(type)
35}
36
37func decode(_ type: UInt.Type, forKey key: Self.Key) throws -> UInt {
38  return try decoder.decode(type)
39}
40
41func decode(_ type: UInt8.Type, forKey key: Self.Key) throws -> UInt8 {
42  return try decoder.decode(type)
43}
44
45func decode(_ type: UInt16.Type, forKey key: Self.Key) throws -> UInt16 {
46  return try decoder.decode(type)
47}
48
49func decode(_ type: UInt32.Type, forKey key: Self.Key) throws -> UInt32 {
50  return try decoder.decode(type)
51}
52
53func decode(_ type: UInt64.Type, forKey key: Self.Key) throws -> UInt64 {
54  return try decoder.decode(type)
55}

So now you understand the basic implementation of a decoder. But how do you actually decode the data? That’s the topic of the next post. Stay tuned!