Introduction
A common misconception with Swift Codable is that optional fields automatically handle type mismatches. They do not. An optional field handles a missing key or a null value, but if the JSON contains a value of the wrong type (e.g., a string where an integer is expected), decoding fails with a typeMismatch error. This frequently occurs when APIs evolve or return inconsistent data types.
Symptoms
typeMismatch(Swift.Int, ... expected to decode Int but found a string/data)dataCorruptederror for a field declared as optional- Decoding succeeds for some records but fails for others
- API sometimes returns
"0"(string) instead of0(number) - Error:
Expected to decode Array<String> but found a dictionary instead
Example error:
``
typeMismatch(Swift.Int,
Swift.DecodingError.Context(
codingPath: [CodingKeys(stringValue: "age", ...)],
debugDescription: "Expected to decode Int but found a string/data instead.",
underlyingError: nil
)
)
Common Causes
- API returns string numbers:
"age": "25"instead of"age": 25 - API returns boolean as integer:
1/0instead oftrue/false - Array returned as single object when there is only one element
- Empty string
""wherenullis expected - Nested object returned as flat string
Step-by-Step Fix
- 1.Handle string-to-number conversion with custom decoder:
- 2.```swift
- 3.struct User: Codable {
- 4.let id: Int
- 5.let name: String
- 6.let age: Int?
enum CodingKeys: String, CodingKey { case id, name, age }
init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(Int.self, forKey: .id) name = try container.decode(String.self, forKey: .name)
// Try Int first, then String -> Int conversion if let intValue = try? container.decode(Int.self, forKey: .age) { age = intValue } else if let stringValue = try? container.decode(String.self, forKey: .age), let parsed = Int(stringValue) { age = parsed } else { age = nil } } } ```
- 1.Create a reusable flexible decoder type:
- 2.```swift
- 3.@propertyWrapper
- 4.struct FuzzyInt: Codable {
- 5.var wrappedValue: Int?
init(wrappedValue: Int?) { self.wrappedValue = wrappedValue }
init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let int = try? container.decode(Int.self) { wrappedValue = int } else if let string = try? container.decode(String.self), let int = Int(string) { wrappedValue = int } else if let double = try? container.decode(Double.self) { wrappedValue = Int(double) } else { wrappedValue = nil } }
func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(wrappedValue) } }
struct User: Codable { let id: Int @FuzzyInt var age: Int? } ```
- 1.Handle boolean-as-integer:
- 2.```swift
- 3.struct Feature: Codable {
- 4.let enabled: Bool
init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let bool = try? container.decode(Bool.self) { enabled = bool } else if let int = try? container.decode(Int.self) { enabled = int != 0 } else if let string = try? container.decode(String.self) { enabled = ["true", "1", "yes"].contains(string.lowercased()) } else { throw DecodingError.typeMismatch( Bool.self, DecodingError.Context(codingPath: container.codingPath, debugDescription: "Expected Bool, Int, or String") ) } } } ```
- 1.**Use
decodeIfPresentfor truly optional fields**: - 2.```swift
- 3.// The key difference:
- 4.let age = try container.decodeIfPresent(Int.self, forKey: .age)
- 5.// This returns nil if the key is absent or null
- 6.// But throws typeMismatch if the value is the wrong type
- 7.
`
Prevention
- Use
@propertyWrappertypes for fuzzy decoding of known API inconsistencies - Add unit tests that decode sample JSON from each API endpoint
- Use
JSONDecoder().dataDecodingStrategyanddateDecodingStrategyconsistently - Log decoding failures with the raw JSON for investigation
- Consider using a library like
FlexibleDecodingfor complex APIs - Document API type inconsistencies and handle them at the model layer