Easier Use of JSON & Codable With POP in Swift

A protocol oriented programming approach to simplify the usual boiler plate when using the Codable protocol in Swift.

Let’s start with a simple type that conforms to Codable. In this case we’ll be using a micro-blog post, as that’s what’s consuming too much of my focus right now.

struct Post: Codable {
	let title: String?
	let content: String
}

Currently most of us are encoding and decoding to/from JSON like this.

let aPost = Post(title: nil, content: "Some Really Interesting Commentary")

// encode as JSON Data
let coder = JSONEncoder()
let data = try? coder.encode(aPost)

// decode from JSON Data
let decoder = JSONDecoder()
let restoredPost = try? decoder.decode(Post.self, from: data!)

This is great and relatively easy, but it is still a fair amount of boiler plate every time. You first need an encoder/decoder and you have to deal with the fact that the Codable API methods are throwing methods. In this example I used the try? optional return method. It’s even more cumbersome if you need more than just an optional for the return values. Then you’re rolling out the do-catch blocks to catch and parse errors, which you totally should be doing if you are consuming JSON data from a web API you don’t control.

With a little bit of POP, AKA Protocol Oriented Programming, magic we can do better. In the cases where optional return values are sufficient anyway. Storing value types to NSUserDefaults or in other ways to disk for retrieval is just such a case. We know with reasonable certainty what we are storing and retrieving, so we don’t really need the error catching pattern.

We start with a protocol our types can conform to. The protocol doesn’t have any requirements, since will be using an extension to provide default functionality for these convenience features.

protocol JSONCodable { }

We only want this default functionality to be available to types that already conform to Codable. We could achieve this by having our protocol be an extension to Codable, but I like these things to be a little more explicit. We don’t want to accidentally forget and use these methods on other Codable types we aren’t as certain that optional returns are sufficient for. To facilitate both of those goals we’ll use our own explicit protocol, JSONCodable and confine our extension to it to types that are also Codable.

This is our protocol extension. It contains a few static variables and 2 methods to make it a simple line of code to get the JSON Data version of our type or to restore an instance of it from JSON Data.

extension JSONCodable where Self: Codable {
	// An encoder and decoder for our convenience methods to use.
	// These are static variables to be used by the types themselves instead of instances.
	static var encoder: JSONEncoder { return JSONEncoder() }
	static var decoder: JSONDecoder { return JSONDecoder() }
	
	// A convenience method to return instances as JSON Data.
	func jsonData() -> Data? {
		return try? Self.encoder.encode(self)
	}
	
	// An convenience init method to create instances of our type from JSON Data.
	init?(jsonData: Data?) {
		guard let data = jsonData,
			let anInstance = try? Self.decoder.decode(Self.self, from: data)
			else { return nil }
		self = anInstance
	}
}

Now we can simply make our sample type above conform to our new protocol.

extension Post: JSONCodable {}

Now getting JSON Data for instances of our type and restoring them from it is much easier.

// Get a JSON Data representation of an instance of our type.
let newData = aPost.jsonData()

// Restoring an instance of our type from JSON Data
let newRecoveredPost = Post(jsonData: newData)

This won’t handle all of our Codable needs, notably when you don’t have certainty of the data model JSON data you need to restore. You should still use the do-catch block to handle errors in those cases, but in the cases where a simple optional return is sufficient we’ve really simplified things.

POP is a hot buzzword right now, but it’s not a magic bullet. It doesn’t solve all of our design and architecture problems, but in many cases it can be used to add shared functionality to types like this example. This code can all be found on Github if you’d like to take a look or use it in your own projects.

Feel free to send feedback or thoughts to me on Twitter or Micro.blog.

*****
Written on