Introduction
Swift's Codable protocol automatically maps JSON to Swift types, but nested arrays with mixed types, optional arrays, or unexpected nesting levels cause typeMismatch decoding errors. When an API returns [["item1", "item2"]] but your model expects [String], or when a field can be either a single object or an array of objects, the default decoder fails. These issues are common with loosely-designed APIs or APIs that changed their response format.
Symptoms
typeMismatch(Swift.Array, context: codingPath [...])decoding errorExpected to decode Array but found a dictionary instead- Optional array property remains nil despite JSON containing data
- Nested array elements decode as wrong type
- API sometimes returns object, sometimes returns array for same field
Error output:
``
DecodingError.typeMismatch(
Swift.Array<Any>,
DecodingError.Context(
codingPath: [CodingKeys("tags")],
debugDescription: "Expected to decode Array but found a string instead.",
underlyingError: nil
)
)
Common Causes
- API field can be string OR array of strings
- Nested array levels differ from model definition
- JSON uses different structure for empty vs populated arrays
- API returns null instead of empty array
- Mixed types within the same JSON array
Step-by-Step Fix
- 1.**Handle field that can be single value or array":
- 2.```swift
- 3.struct Article: Codable {
- 4.let id: Int
- 5.let title: String
- 6.let tags: [String]
enum CodingKeys: String, CodingKey { case id, title, tags }
init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(Int.self, forKey: .id) title = try container.decode(String.self, forKey: .title)
// Tags can be a string or an array of strings if let tagString = try? container.decode(String.self, forKey: .tags) { tags = [tagString] } else { tags = try container.decode([String].self, forKey: .tags) } } }
// Handles both: // {"id": 1, "title": "Hello", "tags": "swift"} // {"id": 1, "title": "Hello", "tags": ["swift", "coding"]} ```
- 1.**Decode nested arrays with custom container":
- 2.```swift
- 3.struct DataResponse: Codable {
- 4.let users: [[User]] // Array of arrays
enum CodingKeys: String, CodingKey { case data }
init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let dataContainer = try container.nestedUnkeyedContainer(forKey: .data)
var userGroups: [[User]] = [] var tempContainer = dataContainer
while !tempContainer.isAtEnd { // Each group is a nested unkeyed container var groupContainer = try tempContainer.nestedUnkeyedContainer() var group: [User] = []
while !groupContainer.isAtEnd { let user = try groupContainer.decode(User.self) group.append(user) }
userGroups.append(group) }
users = userGroups } } ```
- 1.**Handle null or missing arrays gracefully":
- 2.```swift
- 3.struct ApiResponse: Codable {
- 4.let items: [Item]
enum CodingKeys: String, CodingKey { case items }
init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self)
// Try array first, fall back to empty array for null/missing items = (try? container.decode([Item].self, forKey: .items)) ?? [] } }
// Handles: // {"items": [...]} -> decoded normally // {"items": null} -> empty array // {} -> empty array ```
- 1.**Use a property wrapper for flexible array decoding":
- 2.```swift
- 3.@propertyWrapper
- 4.struct FlexibleArray<T: Codable>: Codable {
- 5.var wrappedValue: [T]
init(wrappedValue: [T]) { self.wrappedValue = wrappedValue }
init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer()
// Try as array if let array = try? container.decode([T].self) { wrappedValue = array return }
// Try as single element if let single = try? container.decode(T.self) { wrappedValue = [single] return }
// Default to empty wrappedValue = [] }
func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(wrappedValue) } }
// Usage struct Response: Codable { @FlexibleArray var tags: [String] @FlexibleArray var categories: [Category] } ```
Prevention
- Write Codable tests against real API responses, not hand-crafted JSON
- Use custom
init(from:)for fields with variable types - Add property wrappers for common flexible decoding patterns
- Monitor decoding errors in production to detect API changes
- Use
JSONDecoder().dataDecodingStrategyfor binary data in JSON - Document expected JSON structure in API integration tests