Articles and podcasts about Swift development, by John Sundell.

Genius Scan SDK

Presented by the Genius Scan SDK

Decoding Swift types that require additional data

Published on 30 Jun 2025

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 both Encodable and Decodable), our above Movie 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!

Genius Scan SDK

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.