What would be a very simple working Decoder?
This article is part of a series.
- Part 1: What if I just copy-paste from the web?
- Part 2: How do you get messages to Swift directly?
- Part 3: Okay, but how about all the way up to the View?
- Part 4: How to do some basic file handling?
- Part 5: How do custom Encoder's work?
- Part 6: And what can I make a custom Encoder do?
- Part 7: Wait, how do I scan text again?
- Part 8: Date Parsing. Nose wrinkle.
- Part 9: This Article
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.
- property delimited by “/”
- key-value delimited by “:”
- lenient about whitespace.
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!
- remember how to inspect strings in general
- decide about Dates specifically
- write a really really simple decoder to walk through the process <=
- inspect other peoples decoders
- write the string -> tree step for SimpleCoder
- finish the full decoder (with round trip tests)
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 FixedWidthInteger
s!) 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.
This article is part of a series.
- Part 1: What if I just copy-paste from the web?
- Part 2: How do you get messages to Swift directly?
- Part 3: Okay, but how about all the way up to the View?
- Part 4: How to do some basic file handling?
- Part 5: How do custom Encoder's work?
- Part 6: And what can I make a custom Encoder do?
- Part 7: Wait, how do I scan text again?
- Part 8: Date Parsing. Nose wrinkle.
- Part 9: This Article