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 error
  • Expected 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. 1.**Handle field that can be single value or array":
  2. 2.```swift
  3. 3.struct Article: Codable {
  4. 4.let id: Int
  5. 5.let title: String
  6. 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. 1.**Decode nested arrays with custom container":
  2. 2.```swift
  3. 3.struct DataResponse: Codable {
  4. 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. 1.**Handle null or missing arrays gracefully":
  2. 2.```swift
  3. 3.struct ApiResponse: Codable {
  4. 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. 1.**Use a property wrapper for flexible array decoding":
  2. 2.```swift
  3. 3.@propertyWrapper
  4. 4.struct FlexibleArray<T: Codable>: Codable {
  5. 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().dataDecodingStrategy for binary data in JSON
  • Document expected JSON structure in API integration tests