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:
-
If you don't need a general-case solution for anyT
andU
, you can create a dummy wrapper around<V>[String: V]
which just encodes the base dictionary through asingleValueContainer()
, which will re-inject the opportunity for theEncoder
to see the dictionary as-is and account for it. This does require you to change allComposedEncodable
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 changeSorry, 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: -
For a more general solution and sticking with the existing
Encodable
implementations onT
andU
, you can create a customEncoder
that just produces, say, a[String: Any]
dictionary of the structure of what's encoded into it. You can produce a dictionary forbase
andextra
, merge them manually, and then encode that -
Stepping out of the confines of
Codable
, if you controlT
andU
, you could add the equivalent with atoDict()
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.