CodableWithConfiguration: Advanced JSON Handling Made Simple

👍
· May 25, 2025 · 4 mins read
CodableWithConfiguration: Advanced JSON Handling Made Simple

If have experienced the limitation of standard Codable in Swift. CodableWithConfiguration could be your solution. This is a powerful protocol that provides fine-grained control over the encoding and decoding process through configurable contexts.

The Problem with Standard Codable

While Codable works brilliantly for straightforward JSON mapping, real-world applications often require more flexibility and require us to implement customised encode/decode functions. CodableWithConfiguration extends the capabilities of Codable by introducing a configuration object that can influence encode/decode behaviour. This configuration is passed through the encoding/decoding process, allowing the types to adapt their behavior based on context.

protocol CodableWithConfiguration {
    associatedtype Configuration
    
    init(from decoder: Decoder, configuration: Configuration) throws
    func encode(to encoder: Encoder, configuration: Configuration) throws
}

The key insight here is that the Configuration associated type can be any type we define – a struct, enum, or even a simple value type that carries the contextual information your encoding/decoding logic needs.

Real-World Example: Multi-Environment User Profile

Let’s assume we have two api versions, and in v1, the api return temperatue in celsius while in v2, the api return temperature in fahrenheit. While we always use celsius in the app, so we better to handle this in the encoding/decoding process. For this purpose, we can do the following:

import Foundation
import Foundation

enum APIVersion {
    case v1
    case v2

    var toCelsius: (Double) -> Double {
        switch self {
        case .v1: return { $0 }
        case .v2: return { ($0 - 32) * 5 / 9 } // fahrenheit to celsius
        }
    }

    var toFahrenheitIfNeeded: (Double) -> Double {
        switch self {
        case .v1: return { $0 }
        case .v2: return { $0 * 9 / 5 + 32 } // celsius to fahrenheit
        }
    }
}

struct WeatherForcast: CodableWithConfiguration {
    let date: Date
    let temperature: Double // in celsius

    init(date: Date, temperature: Double) {
        self.date = date
        self.temperature = temperature
    }

    enum CodingKeys: String, CodingKey {
        case date
        case temperature
    }

    init(from decoder: Decoder, configuration: APIVersion) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        date = try container.decode(Date.self, forKey: .date)

        let tempValue = try container.decode(Double.self, forKey: .temperature)
        temperature = configuration.toCelsius(tempValue)
    }

    func encode(to encoder: Encoder, configuration: APIVersion) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(date, forKey: .date)
        try container.encode(configuration.toFahrenheitIfNeeded(temperature), forKey: .temperature)
    }
}

let forcast = WeatherForcast(date: Date(), temperature: 10)
let encoder = JSONEncoder()
let data = try encoder.encode(forcast, configuration: .v2)
print(String(data: data, encoding: .utf8))

let decoder = JSONDecoder()
let decodedForcast = try decoder.decode(WeatherForcast.self, from: data, configuration: .v2)
print(decodedForcast)

With the above setup, we can encode/decode data correctly by providing right api version. If WeatherForcast is part of another type, there’s @CodableConfiguration property wrapper to help provide configuration like below:

struct Weather: Codable {
    @CodableConfiguration(from: WeatherForcastConfiguration.self) var forcast: WeatherForcast? = nil
}

struct WeatherForcastConfiguration: EncodingConfigurationProviding & DecodingConfigurationProviding {
    static var decodingConfiguration: APIVersion = .v1
    static var encodingConfiguration: APIVersion = .v1
}

With this, we can easily update the api version in WeatherForcastConfiguration to switch api version.

When to Use CodableWithConfiguration

Consider CodableWithConfiguration when we have:

  • Multiple API versions to support simultaneously
  • Environment-specific data formatting requirements
  • User preferences that affect data representation
  • Complex business logic that depends on runtime context
  • Testing scenarios that require different data handling behaviors

However, don’t reach for it immediately. Standard Codable is simpler and sufficient for most use cases. Use CodableWithConfiguration when we find ourselves writing complex conditional logic within the customised Codable implementations or maintaining multiple similar model types.

Conclusion

CodableWithConfiguration provides the flexibility needed for complex, real-world applications while maintaining the type safety and performance characteristics that make Swift’s Codable system so powerful. By embracing configuration-driven encoding and decoding, we can build more maintainable, testable, and flexible applications that adapt gracefully to changing requirements. The key is to use it judiciously – when we need the power it provides, it’s invaluable, but simpler solutions should be preferred when they suffice.

Comments

Sharing is caring!