AnyEncodable: Encoding unknown types

In this post, I show to encode unknown types that conform to Encodable

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 URLs 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.