How to encode objects of unknown type?

Sure, happy to elaborate!

Did you include the necessary extension on Encodable?

extension Encodable {
    fileprivate func encode(to container: inout SingleValueEncodingContainer) throws {
        try container.encode(self)
    }
}

If you did and this still isn't compiling for you, can you show your full code? (And what version of Swift are you using?)

To elaborate on this point: when you execute value.encode(to: encoder), you are directly invoking value's encode(to:) method, which directly allows it to request containers and encode in its preferred representation. For the vast majority of types out there, this behaves as expected.

This breaks down, though, for any type T whose preferred representation differs from the Encoder's preferred representation for the type. Let's use URL as an example, whose encode(to:) reads as follows:

public func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(self.relativeString, forKey: .relative)
    if let base = self.baseURL {
        try container.encode(base, forKey: .base)
    }
}

URL itself prefers a keyed container which allows it to encode its base and relative string separately, to preserve them. This representation makes sense for URL itself, but it would be pretty surprising to encode a URL with JSONEncoder and get out a dictionary (e.g. {"relative": "http://swift.org"}) rather than a string representation ("http://swift.org"). JSONEncoder, then, intercepts the URL type when it is encoded to encode the preferred string representation:

fileprivate func box_<T : Encodable>(_ value: T) throws -> NSObject? {
    // ...
    if T.self == URL.self || T.self == NSURL.self {
        // Encode URLs as single strings.
        return self.box((value as! URL).absoluteString)
    }

    // ...
}

This works, of course, only when the encoder gets a chance to see that what's being encoded is a URL.

So the key difference is that myURL.encode(to: encoder) asks URL to encode directly into the encoder by requesting a keyed container, while encoder.singleValueContainer().encode(myURL) gives the encoder a chance (via the single-value container) to see that what's actually being encoded is a URL instead of just seeing its contents.

The code above offers the latter option via try container.encode(self) rather than the simpler self.encode(to: container). You can see the effect of this in the following:

import Foundation

extension Encodable {
    fileprivate func encode(to container: inout SingleValueEncodingContainer) throws {
        try container.encode(self)
    }
}

struct AnyEncodable1 : Encodable {
    var value: Encodable
    init(_ value: Encodable) {
        self.value = value
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try value.encode(to: &container)
    }
}

struct AnyEncodable2 : Encodable {
    var value: Encodable
    init(_ value: Encodable) {
        self.value = value
    }
    func encode(to encoder: Encoder) throws {
        try value.encode(to: encoder)
    }
}

struct MyThing : Encodable {
    let myURL = AnyEncodable1(URL(string: "http://swift.org")!)
}

let url = URL(string: "http://swift.org")
let encoder = JSONEncoder()
var data = try encoder.encode(MyThing())
print(String(data: data, encoding: .utf8)!) // => {"myURL":"http:\/\/swift.org"}

Changing the above to

struct MyThing : Encodable {
    let myURL = AnyEncodable2(URL(string: "http://swift.org")!)
}

produces {"myURL":{"relative":"http:\/\/swift.org"}}.

The container should always be a singleValueContainer(). Encoding a value into a single-value container is equivalent to encoding the value directly into the encoder, with the primary difference being the above: encoding into the encoder writes the contents of a type into the encoder, while encoding to a single-value container gives the encoder a chance to intercept the type as a whole.

(I can elaborate on this point if it would help.)

6 Likes