The Swift Encodable
protocol provides a means for us to encode our custom types into JSON. It is possible to encode into other formats like XML, but this post focuses only on JSON encoding.
To encode a type into JSON, the type must conform to Encodable
, and we can then
call
JSONEncoder.encode(<custom_type_instance>)
For example, if we have a struct defined as follows:
struct MyType: Encodable {
var prop1: String
var prop2: String
}
we can encode an instance of MyType
as shown below:
let myType = MyType(prop1: "Hey", prop2: "There!")
// produces { "prop1": "Hey", "prop2": "There!" }
let data = try JSONEncoder.encode(myType)
This works nicely when we know the exact type we are trying to encode,
but what if we wanted to encode any type that conforms to Encodable
?
This was the problem I ran into last week, and I wanted to share the
solution I arrived at, and also a more accurate solution.
Problem
The problem here is that we want to make this line of code compile:
func serialize(model: Encodable) throws -> Data {
return try JSONEncoder().encode(model)
}
Currently as is, the block of code above produces an error:
Protocol type 'Encodable' cannot conform to 'Encodable'
because only concrete types can conform to protocols
This happens because the concrete type of model
gets erased to Encodable
, and the compiler needs to know the concrete type in order to know which encode
function to invoke. So how do we get around this limitation?
Solution #1
The first solution, which is the solution I arrived at, and also the most common solution I encountered while searching on the internet was to wrap the encodable model into an AnyEncodable
Box:
struct AnyEncodable: Encodable {
let value: Encodable
func encode(to encoder: Encoder) throws {
try self.value.encode(to: encoder)
}
}
This works in most cases, but it has a limitation:
Some types like URL
or Date
behave differently depending on the type of Encoder
used. URL
by default prefers to preserve it's "base" and "relative" information when being encoded i.e Encoding
URL(string: "stuff",
relativeTo: URL(string: "http://yourfriendlyioscoder.com")!)!
// => {"base":"http:\/\/yourfriendlyioscoder.com","relative":"stuff"}
But when using a JSONEncoder
, URL
gets encoded into a regular string with the absolute URL. By using the AnyEncodable
definition above, we bypass the encoder-specific behaviour and go directly to the encoding behaviour defined by the type. This will cause URL
s to be printed as shown above in some cases, and this is not what we want. So how do we get around this limitation?
Solution #2
This solution builds on top of Solution 1, but also fixes the limitation that was highlighted above. This time, we define an AnyEncodable
Box as follows:
struct AnyEncodable: Encodable {
let value: Encodable
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try value.encode(to: &container)
}
}
extension Encodable {
func encode(to container: inout SingleValueEncodingContainer) throws {
try container.encode(self)
}
}
The solution above is to request a singleValueContainer
in which we will encode the wrapped model into. By doing this, we still give the Encoder a chance to intercept a particular types encoding behaviour, replacing it with an encoding behaviour that suites the Encoder
. By using this AnyEncodable
, URL
will be encoded the expected way when using a JSONEncoder
.
Bringing it all together
With the AnyEncodable
we arrived at in Solution #2, we can then use it to solve our initial problem:
func serialize(model: Encodable) throws -> Data {
let encodableBox = AnyEncodable(value: model)
return try JSONEncoder().encode(encodableBox)
}
We wrap the model
parameter in our defined AnyEncodable
, and then encode it. 🚀
Conclusion
I hope this post helps you understand a little bit more about Encodable
, and also showed you how to encode unknown types that conform to Encodable
.
As much as I would like to take the credit for Solution #2, it was solution I discovered from a question in the Swift Forums.
Thanks for taking the time. 🙏🏿
Find me on twitter or contact me if you have any questions or suggestions.