Skip to main content

Mike Kreuzer

Is serializable really a word?

30 September 2017

Swift 4 brought with it two things I've been waiting for since Swift's early days: multi-line strings and JSON serialisation. I've seen a few half implementations of a serialising protocol extensions recently, so for completeness here's a fuller version, with failable initialisation, reading & writing to files, and some date sanity.

import Foundation

protocol Serializable: Codable {
    init?(from source: Data?)
    init?(contentsOf fileURL: URL?) throws
    func save() -> Data?
    func save(to fileURL: URL) throws
}

extension Serializable {
    init?(from source: Data?) {
        guard let source = source else {
            return nil
        }
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        if let val = try? decoder.decode(Self.self, from: source) {
            self = val
        } else {
            return nil
        }
    }
    init?(contentsOf fileURL: URL?) throws {
        guard let fileURL = fileURL else {
            return nil
        }
        self.init(from: try Data(contentsOf: fileURL))
    }
    func save() -> Data? {
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        return try? encoder.encode(self)
    }
    func save(to fileURL: URL) throws {
        guard let data = self.save() else {
            return
        }
        try data.write(to: fileURL, options: .atomic)
    }
}

(And I forgot to put decoder.dateDecodingStrategy in there when I first published this. Damn.)

I went with init and save rather than serialize and deserialize partly because init felt right, but also because serialize and deserialize inevitably make me scratch my head about which is which. I'm still in two minds about whether to swallow those error throws, or to demand an unwrapped URL… maybe, I mostly tend to use throws for disk operations so this mix feels right for now.

From then on serialisation is a simple function call anywhere you invoke the protocol:

struct SerializableStruct: Serializable {
    let name: String
}

let myStruct = SerializableStruct(name: "ok")
let data = myStruct.save()
let aStructAgain = SerializableStruct(from: data)

UPDATE - June 7, 2018 – The following is solved now, see below

The only issue I have so far – and this is minor compared to the joy I feel at the thought of dozens of Swift to JSON libraries finally being killed by Codable – is that collections don't automagically know about this. Protocol "inheritance" only works down the chain, so for example an array of structs which are declared to be serializable has to be encoded and decoded "manually", or else wrapper inside another struct or class that is itself serializable, so:

// eg for
let plainArray = [myStruct]

//either
struct ArrayWrapper: Serializable {
    let wrapper: [SerializableStruct]
}
let wrappedArray = ArrayWrapper(wrapper: plainArray)
let dataYetAgain = wrappedArray.save()

// or
let dataYetAgainAgain = try? JSONEncoder().encode(plainArray)

// etc

Unless I'm doing something wrong of course!

UPDATED FOR Swift 4.2 - June 7, 2018

Nope, not doing anything wrong until earlier this week with Swift 4.2. Applying the extension to Array when its elements are Serializable "just works" now, which is very, very nice.

Continuing the original example:

// new in 4.2
extension Array: Serializable where Element: Serializable {}

let myStruct2 = SerializableStruct(name: "woo hoo!")
let plainArray = [myStruct, myStruct2]
let dataAgain = plainArray.save()
let arrayAgain = Array<SerializableStruct>(from: dataAgain)
Tags: