Aggregating two Encodable's

I've just encountered the following in our codebase:

public struct ComposedEncodable<T: Encodable, U: Encodable>: Encodable {
    let base: T
    let extra: U

    public func encode(to encoder: Encoder) throws {
        try base.encode(to: encoder)
        try extra.encode(to: encoder)

It expects that both base and extra will be encoded using KeyedEncodingContainer, and final result will be a dictionary with keys and values from both.

I'm not sure if this is a correct implementation of the Encoder protocol, but it works with JSONEncoder and a custom encoder.

Normally when [String: String] is encoded by JSONEncoder, key conversions are not applied. But when [String: String] is encoded as extra inside ComposedEncodable, conversions are applied causing a bug.

Is this a correct conformance to Encoder protocol? Is there a way to selectively opt-out from key conversion in JSONEncoder?

Ah, merging Encodable values like this is really tricky business, and not really well-supported through Codable itself.

This is the first sign that this might not be quite the right approach. Specifically, it's a hard error to request and encode into two different container types from the same encoder, so if you accidentally create a ComposedEncodable where base encodes into a keyed container and extra encodes into an unkeyed container, you're likely going to fatalError. (This is guaranteed with JSONEncoder.)

This is the second sign that this isn't a great way to do this. I won't rehash it here, but Propery wrapper decoding difference between `singleValueContainer` and `init(from: Decoder)` offers a good overview of the pitfalls of calling encode(to:)/init(from:) directly on an object instead of encoding/decoding into a container — specifically, when you encode directly this way, an encoder doesn't get the opportunity to intercept the object being encoded, and it can't apply encoding strategies (or in the case of String-keyed dictionaries, opt you out of the key conversion strategy).

So what is the right way to do this? It depends on what sort of control you might have on T and U. Codable itself doesn't offer tools for doing this because you can't guarantee that T and U will want the same container type, but if you reasonably know this will be the case ahead of time, you might have some options:

  1. If you don't need a general-case solution for any T and U, you can create a dummy wrapper around <V>[String: V] which just encodes the base dictionary through a singleValueContainer(), which will re-inject the opportunity for the Encoder to see the dictionary as-is and account for it. This does require you to change all ComposedEncodable instances which have bare dictionaries (and nothing will protect you from forgetting to apply it in the future), but it can be a pretty minimal change

    Sorry, I had gotten this wrong — you'd end up with two values encoding into singleValueContainer() which is similarly disallowed. Creating a wrapper around <V>[String: V] which encodes into a keyed container also won't help, because the encoder won't be aware that you're actually encoding a dictionary. I'd recommend starting with one of the solutions below:

  2. For a more general solution and sticking with the existing Encodable implementations on T and U, you can create a custom Encoder that just produces, say, a [String: Any] dictionary of the structure of what's encoded into it. You can produce a dictionary for base and extra, merge them manually, and then encode that

  3. Stepping out of the confines of Codable, if you control T and U, you could add the equivalent with a toDict() method or similar which produces the same result, merge those, and encode that

There are some other approaches you could take, but they're more complex. Depending on the T and U, this might be enough.

Terms of Service

Privacy Policy

Cookie Policy