What would be a very simple working Decoder?

This article is part of a series.

After finding a couple of great public examples of simple Encoders (swift talk | stack overflow), I did not find the same for a Decoder. Here’s my shot at it. Like the very good but behind a paywall swift talk example, it has limited use. Both were designed with a single type in mind. This one can decode a single serialized struct with Strings, Ints and Dates as properties.

The SimpleCoder Decoder will need a much more complicated parser and will handle more types.

So with a post about string munching and one about date parsing in the rear-view let’s make a Decoder!

Finished playground code

Using the Decoder

First updating the struct to actually use Date in the struct and add an Int for good measure.

struct HousePlant:Codable {
    let commonName:String
    let whereAcquired:String?
    let dateAcquired:Date
    //NOTE THE VAR. `let` with a default wil not be decoded.
    var currentPropCount:Int = 0 
    let dateOfDeath:Date?
}

Thanks to some handy code completions in XCode we can peak at the synthesized Codable conformance:

    enum CodingKeys: CodingKey {
        case commonName
        case whereAcquired
        case dateAcquired
        case currentPropCount
        case dateOfDeath
    }
    
    init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.commonName = try container.decode(String.self, forKey: .commonName)
        self.whereAcquired = try container.decodeIfPresent(String.self, forKey: .whereAcquired)
        self.dateAcquired = try container.decode(Date.self, forKey: .dateAcquired)
        self.currentPropCount = try container.decodeIfPresent(Int.self, forKey: .currentPropCount)
        self.dateOfDeath = try container.decodeIfPresent(Date.self, forKey: .dateOfDeath)
    }
    
    func encode(to encoder: any Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.commonName, forKey: .commonName)
        try container.encodeIfPresent(self.whereAcquired, forKey: .whereAcquired)
        try container.encode(self.dateAcquired, forKey: .dateAcquired)
        try container.encode(self.currentPropCount, forKey: .currentPropCount)
        try container.encodeIfPresent(self.dateOfDeath, forKey: .dateOfDeath)
    }

From the init(from decoder: any Decoder) one can tell that the Decoder needs to be able to produce values when given a CodingKey and a Type.

Using that init would look like:

//Have raw input
let serializedHousePlant = "commonName:spider plant/whereAcquired:Trader Joe's/dateAcquired: 2024-03-12T16:12:07Z / currentPropCount:8 "
//have a decoder that has a chance of doing the lexical analysis
let decoder = HousePlantDecoder(serializedHousePlant)
//Codable finishes 
let myHousePlant = try HousePlant(from: decoder)

Our decoder holds a potential HousePlant in its belly or BUST.

The Data Type

Using the same reference-box as SimpleCoder

final class SimpleCoderData<Value> {
    var storage:Value
    
    init(_ value: Value) {
        self.storage = value
    }
}

Declare and conform

Notice the fatalError on the SingleValueContainer and UnkeyedContainer? Well, a single serialized HousePlant will never need them.

struct HousePlantDecoder:Decoder {
    var data:SimpleCoderData<Dictionary<String,String>>
    
    var codingPath: [any CodingKey] = []
    var userInfo: [CodingUserInfoKey : Any] = [:]
    
    func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
        KeyedDecodingContainer(HousePlantDecoderKDC(decoder: self))
    }
    
    func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
        fatalError()
    }
    
    func singleValueContainer() throws -> any SingleValueDecodingContainer {
        fatalError()
    }
    
}

Parsing initializer

Can a Dictionary<String,String> even happen? If not, I don’t even want to bother making the Decoder.

The second parser from the string scanning post nicely balances speed and complexity.

extension HousePlantDecoder {
    static func parse(_ inputString:String) throws -> Dictionary<String,String> {
        let fullKeyValueList = inputString.split(separator: "/")
        var result:Dictionary<String,String> = [:]
        return try fullKeyValueList.reduce(into: result){ result, item in
            if let firstColonIndex = item.firstIndex(of: ":") {
                let key = item
                    .prefix(upTo: firstColonIndex)
                    .trimmingCharacters(in: .whitespacesAndNewlines)
                let value = item
                    .suffix(from: item.index(firstColonIndex, offsetBy: 1))
                    .trimmingCharacters(in: .whitespacesAndNewlines)
                result[String(key)] = String(value)
            } else {
                throw HousePlantError.notAKeyValuePair
            }
        }
    }

    init(_ inputString:String) throws {
        self.data = SimpleCoderData(try HousePlantDecoder.parse(inputString))
    }
}

Custom Functions for Containers

The containers will need to retrieve values from the storage, and throw errors if the values aren’t there.

Also, the date decoder from the previous post should go in there since our date isn’t in timeIntervalSinceReferenceDate format (the default).

extension HousePlantDecoder {
    func retrieveValue(forKey codingKey:CodingKey) throws -> String {
        guard let value = data.storage[codingKey.stringValue] else {
            throw DecodingError.keyNotFound(codingKey, DecodingError.Context(codingPath: codingPath, debugDescription: "Didn't find key in decoder data."))
        }
        return value
    }

    func _decodeDate(from value:String) throws -> Date {
        let format = if value.contains(":") {
                        Date.ISO8601FormatStyle.iso8601
                    } else {
                        Date.ISO8601FormatStyle.iso8601
                            .year().month().day()
                    }
        guard let date = try? format.parse(value) else {
            throw DecodingError.dataCorrupted(
                .init(codingPath: [], debugDescription: "String not in expected \(format.parseStrategy) format.")
            )
        }
        return date
    }
}

KeyedDecodingContainerProtocol

Almost the bare minimum to get it working.

struct HousePlantDecoderKDC<Key:CodingKey>:KeyedDecodingContainerProtocol {
    let decoder:HousePlantDecoder
    var codingPath: [any CodingKey] { decoder.codingPath }
    
    var allKeys: [Key] {
        decoder.data.storage.keys.compactMap({ Key(stringValue: $0) })
    }
    
    func contains(_ key: Key) -> Bool {
        decoder.data.storage.keys.contains(key.stringValue)
    }
    
    func decodeNil(forKey key: Key) throws -> Bool {
        //return true if the value should be .none
        //false if .some, and an attempt should be made to decode the
        //wrapped value
        if decoder.data.storage.keys.contains(key.stringValue) {
            let value = decoder.data.storage[key.stringValue]
            return value == "NULL" || value == "nil"
        } else {
            return true
        }
    }
    
    func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { fatalError() }
    
    func decode(_ type: String.Type, forKey key: Key) throws -> String {
        try decoder.retrieveValue(forKey: key)
    }
    
    func decode(_ type: Double.Type, forKey key: Key) throws -> Double { fatalError() }
    func decode(_ type: Float.Type, forKey key: Key) throws -> Float { fatalError() }

    func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
        let value = try decoder.retrieveValue(forKey: key)
        if let int = Int(value) { return int }
        throw DecodingError.typeMismatch(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Could not make \(type) from \(value)"))
    }

    func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { fatalError() }
    func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { fatalError() }
    func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { fatalError() }
    func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { fatalError() }
    func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { fatalError() }
    func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { fatalError() }
    func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { fatalError() }
    func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { fatalError() }
    func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { fatalError() }
    
    func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
        switch type {
        case is Bool.Type: return try decode(Bool.self, forKey: key) as! T
        case is String.Type: return try decode(String.self, forKey: key) as! T
        case is Double.Type: return try decode(Double.self, forKey: key) as! T
        case is Float.Type: return try decode(Float.self, forKey: key) as! T
        case is Int.Type: return try decode(Int.self, forKey: key) as! T
        case is Int8.Type: return try decode(Int8.self, forKey: key) as! T
        case is Int16.Type: return try decode(Int16.self, forKey: key) as! T
        case is Int32.Type: return try decode(Int32.self, forKey: key) as! T
        case is Int64.Type: return try decode(Int64.self, forKey: key) as! T
        case is UInt.Type: return try decode(UInt.self, forKey: key) as! T
        case is UInt8.Type: return try decode(UInt8.self, forKey: key) as! T
        case is UInt16.Type: return try decode(UInt16.self, forKey: key) as! T
        case is UInt32.Type: return try decode(UInt32.self, forKey: key) as! T
        case is UInt64.Type: return try decode(UInt64.self, forKey: key) as! T
        case is Date.Type: 
            let value = try decoder.retrieveValue(forKey: key)
            print(value)
            return try decoder._decodeDate(from: value) as! T
        default:
            //probably should fatalError this too, but, YOLO.  
            let value = try decoder.retrieveValue(forKey: key)
            let tmpDecoder = try HousePlantDecoder(value)
            return try type.init(from:tmpDecoder)
        }
    }
    
    func decodeIfPresent<T>(_ type: T.Type, forKey key: Key) throws -> T? where T: Decodable {
      guard contains(key) else { return nil }
      return try decode(type, forKey: key)
    }
    
    func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey { fatalError() }
    func nestedUnkeyedContainer(forKey key: Key) throws -> any UnkeyedDecodingContainer { fatalError() }
    func superDecoder(forKey key: Key) throws -> any Decoder { fatalError() }
    func superDecoder() throws -> any Decoder {
        decoder
    }
}

Alteratively, the folks who wrote the Vapor PlaintextDecoder got a little fancy and decoded anything that’s a LosslessStringConvertible in one swoop. (All the FixedWidthIntegers!) I like it. That’s what I’ve done in the code linked at the top and bottom of this post. (I wanted to show the code completion code first)

// in func decode<T>(_ type: T.Type, forKey key: Key)
        case is LosslessStringConvertible.Type :
            let value = try decoder.retrieveValue(forKey: key)
            if let convertible = T.self as? LosslessStringConvertible.Type {
                return try decoder._decodeLossless(value, ofType:convertible) as! T
            } else {
                throw DecodingError.dataCorruptedError(
                    forKey: key,
                    in: self,
                    debugDescription: "Thought \(value) could be converted to \(type.self) as a LosslessStringConvertible. Something went wrong.")
            }

//add to decoder
    func _decodeLossless<L: LosslessStringConvertible>(_ value:String, ofType: L.Type) throws -> L {
        guard let result = L.init(value) else { 
            throw DecodingError.typeMismatch(
                L.self, 
                DecodingError.Context(codingPath: codingPath, 
                                      debugDescription: "Attempt to decode \(value) as \(L.self) failed.")
                ) 
            }
        return result
    }

Summary

Put that all together and one gets a HousePlant struct from a String. the most basic of Decoders, just to prove to myself I get the gist before I go hunting for Decoders on github to break down.

Playground Code

This article is part of a series.