Decoding Swift types that require additional data
Swift’s Codable
API — which consists of the Encodable
protocol for encoding, and Decodable
for decoding — offers a powerful, built-in mechanism for converting native Swift types to and from a serialized format, such as JSON. Thanks to its integration with the Swift compiler, we often don’t have to do any additional work to enable one of our types to become Codable
, such as this Movie
type:
struct Movie: Identifiable, Codable {
let id: UUID
var title: String
var releaseDate: Date
var genre: Genre
var directorName: String
}
Just by adding that
Codable
conformance (which is a type alias for bothEncodable
andDecodable
), our aboveMovie
type can now be serialized and deserialized automatically, as long as the data format (such as JSON) that we’re working with follows the same structure as our Swift type declaration.
However, sometimes we might be working with a type that requires some additional data that’s not present in the JSON (or whichever data format we’re decoding from) in order to be initialized. For example, the following User
type includes a favorites
property — which is a Favorites
value that contains the user’s favorites, such as their favorite director and movie genre:
struct User: Identifiable {
let id: UUID
var name: String
var membershipPoints: Int
var favorites: Favorites
}
struct Favorites: Codable {
var genre: Genre
var directorName: String
var movieIDs: [Movie.ID]
}
However, the JSON response that our app receives from the server when loading the data for a user doesn’t include the Favorites
data, which instead need to be loaded from a separate server endpoint:
// User server response:
{
"id": "7CBE0CC1-7779-42E9-AAF1-C4B145F3CAE9",
"name": "John Appleseed",
"membershipPoints": 192
}
// Favorites server response:
{
"genre": "action",
"directorName": "Christopher Nolan",
"movieIDs": [
"F028CAB5-74D7-4B86-8450-D0046C32DFA0",
"D2657C95-1A35-446C-97D4-FAAA4783F2AA",
"5159AF60-DF61-4A0C-A6BA-AE0E027E2BC2"
]
}
Now the question is, how do we make User
conform to Codable
(or more specifically, Decodable
) without being able to decode the required Favorites
data from the server’s JSON response?
One option would be to simply make the favorites
property optional — but that would have several downsides. First, it would make our data model more fragile, as we could easily miss to populate that property when loading User
values within various contexts (and the compiler wouldn’t be able to warn us about it). Second, and arguably more important, is that we’d constantly have to unwrap that optional favorites
value every time we access it, leading to either extra boilerplate code (and potentially ambiguous states), or dangerous force unwrapping.
Another, more robust option would be to use a secondary, partial model when decoding our User
data, which we would then combine with a Favorites
value in order to form our final model — like this:
extension User {
struct Partial: Decodable {
let id: UUID
var name: String
var membershipPoints: Int
}
}
struct Networking {
var session = URLSession.shared
...
func loadUser(withID id: User.ID) async throws -> User {
let favoritesURL = favoritesURLForUser(withID: id)
let userURL = urlForUser(withID: id)
// Load the user's favorites and the partial user data
// that our server responds with:
async let favorites = request(favoritesURL) as Favorites
async let partialUser = request(userURL) as User.Partial
// Form our final user model by combining the partial
// model with the favorites that were loaded:
return try await User(
id: partialUser.id,
name: partialUser.name,
membershipPoints: partialUser.membershipPoints,
favorites: favorites
)
}
...
private func request<T: Decodable>(_ url: URL) async throws -> T {
let (data, _) = try await session.data(from: url)
return try JSONDecoder().decode(T.self, from: data)
}
}
While the above works perfectly fine, it would be really nice to find a solution that doesn’t require us to duplicate all of our User
model’s properties by declaring a separate, decoding-specific Partial
model. Thankfully, the Swift Codable system* does actually include such a solution — the somewhat lesser known CodableWithConfiguration
API.
* CodableWithConfiguration is not technically a direct part of Codable, which is defined within Swift’s standard library, but is instead an extension defined within Foundation. That doesn’t make much of a difference when targeting any of Apple’s platforms, though.
When a type conforms to either EncodableWithConfiguration
or DecodableWithConfiguration
, it requires an additional configuration value to be passed when either encoding or decoding it (and the compiler will enforce that requirement). That’s incredibly useful in situations such as when decoding our User
type, since we can define that Favorites
is the required DecodingConfiguration
for our type — meaning that we can ensure that such a value will always be present during decoding, without having to declare any additional partial types.
So let’s go ahead and update our User
type to conform to DecodableWithConfiguration
, which does require a manual decoding implementation, unfortunately:
extension User: Encodable, DecodableWithConfiguration {
enum CodingKeys: CodingKey {
case id
case name
case membershipPoints
}
init(from decoder: Decoder, configuration: Favorites) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
membershipPoints = try container.decode(
Int.self,
forKey: .membershipPoints
)
favorites = configuration
}
}
So we still have to write a bit of boilerplate in order to enable our new decoding setup, but the advantage is that we can now make our networking code a lot simpler — all that we need is another overload of our private request
method, which works with types conforming to DecodableWithConfiguration
, and we’ll now be able to leverage type inference to make our decoding call site a lot simpler:
struct Networking {
...
func loadUser(withID id: User.ID) async throws -> User {
let favoritesURL = favoritesURLForUser(withID: id)
let userURL = urlForUser(withID: id)
return try await request(userURL, with: request(favoritesURL))
}
...
private func request<T: Decodable>(_ url: URL) async throws -> T {
...
}
private func request<T: DecodableWithConfiguration>(
_ url: URL,
with config: T.DecodingConfiguration
) async throws -> T {
let (data, _) = try await session.data(from: url)
return try JSONDecoder().decode(
T.self,
from: data,
configuration: config
)
}
}
However, one thing that’s a bit puzzling about the Codable WithConfiguration
API is that even though the protocol itself, as well as the KeyedCodingContainer
methods that enable us to perform nested decoding of such types, are all available from iOS 15, the top-level configuration-compatible JSONDecoder
API wasn’t added until iOS 17.
Thankfully, that’s something that we can quite easily work around if working on a project that needs to support iOS 16 and earlier — by introducing our own implementation of that API, which uses Codable’s userInfo
mechanism to store the configuration of the value that we’re currently decoding:
extension JSONDecoder {
// First, we define a wrapper type which we'll use to decode
// values that require a configuration type:
private struct ConfigurationDecodingWrapper<
Wrapped: DecodableWithConfiguration
>: Decodable {
var wrapped: Wrapped
init(from decoder: Decoder) throws {
let configuration = decoder.userInfo[configurationUserInfoKey]
wrapped = try Wrapped(
from: decoder,
configuration: configuration as! Wrapped.DecodingConfiguration
)
}
}
private static let configurationUserInfoKey = CodingUserInfoKey(
rawValue: "configuration"
)!
// Then, we declare our own decode method (which omits the
// type parameter in order to not conflict with the built-in
// API), which will work on iOS 15 and above:
func decode<T: DecodableWithConfiguration>(
from data: Data,
configuration: T.DecodingConfiguration
) throws -> T {
let decoder = JSONDecoder()
decoder.userInfo[Self.configurationUserInfoKey] = configuration
let wrapper = try decoder.decode(
ConfigurationDecodingWrapper<T>.self,
from: data
)
return wrapper.wrapped
}
}
CodableWithConfiguration
is really quite useful when using Swift’s built-in serialization API to encode and decode types that require additional data in order to be initialized, without having to resort to modeling required data as optional, or having to define additional types that are only ever used for decoding purposes.
I hope that you found this article useful. Feel free to reach out via either either Mastodon or Bluesky if you have any questions or feedback.
Thanks for reading!

Swift by Sundell is brought to you by the Genius Scan SDK — Add a powerful document scanner to any mobile app, and turn scans into high-quality PDFs with one line of code. Try it today.