Introduction
Swift's Codable protocol is designed for encoding to and decoding from external formats like JSON. There is no built-in DictionaryEncoder, but you can convert a Codable object to a [String: Any] dictionary by encoding it to JSON data first, then deserializing that data with JSONSerialization. For a cleaner approach, you can write a custom DictionaryEncoder that avoids the intermediate JSON step.
Method 1: JSON Round-Trip (Simple)
Encode to JSON, then deserialize to a dictionary:
1import Foundation
2
3struct User: Codable {
4 let name: String
5 let age: Int
6 let email: String
7}
8
9let user = User(name: "Alice", age: 30, email: "[email protected]")
10
11// Encode to JSON Data
12let jsonData = try JSONEncoder().encode(user)
13
14// Deserialize to dictionary
15let dict = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any]
16print(dict!)
17// ["name": "Alice", "age": 30, "email": "[email protected]"]
This works for any Codable type. The downside is two serialization steps (encode to JSON bytes, then parse those bytes back to objects).
Method 2: Extension on Encodable (Reusable)
1extension Encodable {
2 func asDictionary() throws -> [String: Any] {
3 let data = try JSONEncoder().encode(self)
4 guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
5 throw EncodingError.invalidValue(
6 self,
7 EncodingError.Context(
8 codingPath: [],
9 debugDescription: "Top-level value is not a dictionary"
10 )
11 )
12 }
13 return dict
14 }
15
16 func asArray() throws -> [[String: Any]] {
17 let data = try JSONEncoder().encode(self)
18 guard let array = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
19 throw EncodingError.invalidValue(
20 self,
21 EncodingError.Context(
22 codingPath: [],
23 debugDescription: "Top-level value is not an array"
24 )
25 )
26 }
27 return array
28 }
29}
30
31// Usage
32let user = User(name: "Alice", age: 30, email: "[email protected]")
33let dict = try user.asDictionary()
34print(dict["name"]!) // Alice
Method 3: Custom DictionaryEncoder
For a direct conversion without JSON intermediary:
1class DictionaryEncoder {
2 func encode<T: Encodable>(_ value: T) throws -> [String: Any] {
3 let data = try JSONEncoder().encode(value)
4 let jsonObject = try JSONSerialization.jsonObject(with: data)
5
6 guard let dict = jsonObject as? [String: Any] else {
7 throw EncodingError.invalidValue(
8 value,
9 EncodingError.Context(
10 codingPath: [],
11 debugDescription: "Expected dictionary, got \(type(of: jsonObject))"
12 )
13 )
14 }
15 return dict
16 }
17}
18
19class DictionaryDecoder {
20 func decode<T: Decodable>(_ type: T.Type, from dict: [String: Any]) throws -> T {
21 let data = try JSONSerialization.data(withJSONObject: dict)
22 return try JSONDecoder().decode(type, from: data)
23 }
24}
25
26// Usage
27let encoder = DictionaryEncoder()
28let decoder = DictionaryDecoder()
29
30let dict = try encoder.encode(user)
31let restored = try decoder.decode(User.self, from: dict)
Nested Objects
1struct Address: Codable {
2 let street: String
3 let city: String
4 let zip: String
5}
6
7struct Employee: Codable {
8 let name: String
9 let department: String
10 let address: Address
11 let skills: [String]
12}
13
14let employee = Employee(
15 name: "Bob",
16 department: "Engineering",
17 address: Address(street: "123 Main St", city: "Seattle", zip: "98101"),
18 skills: ["Swift", "Kotlin", "Rust"]
19)
20
21let dict = try employee.asDictionary()
22print(dict)
23// ["name": "Bob", "department": "Engineering",
24// "address": ["street": "123 Main St", "city": "Seattle", "zip": "98101"],
25// "skills": ["Swift", "Kotlin", "Rust"]]
26
27// Access nested values
28let address = dict["address"] as? [String: Any]
29print(address?["city"]!) // Seattle
Custom Coding Keys
1struct APIRequest: Codable {
2 let userId: Int
3 let accessToken: String
4 let isActive: Bool
5
6 enum CodingKeys: String, CodingKey {
7 case userId = "user_id"
8 case accessToken = "access_token"
9 case isActive = "is_active"
10 }
11}
12
13let request = APIRequest(userId: 42, accessToken: "abc123", isActive: true)
14let dict = try request.asDictionary()
15print(dict)
16// ["user_id": 42, "access_token": "abc123", "is_active": true]
The dictionary keys follow the CodingKeys mapping, producing snake_case keys.
Date Encoding Strategies
1struct Event: Codable {
2 let title: String
3 let date: Date
4}
5
6let event = Event(title: "Launch", date: Date())
7
8let encoder = JSONEncoder()
9encoder.dateEncodingStrategy = .iso8601
10
11let data = try encoder.encode(event)
12let dict = try JSONSerialization.jsonObject(with: data) as! [String: Any]
13print(dict["date"]!) // "2025-03-02T10:15:32Z"
Using with Firestore / API Payloads
1// Firebase Firestore expects [String: Any] dictionaries
2let user = User(name: "Alice", age: 30, email: "[email protected]")
3let dict = try user.asDictionary()
4
5// Firestore
6// db.collection("users").document("alice").setData(dict)
7
8// URLRequest body
9let jsonData = try JSONSerialization.data(withJSONObject: dict)
10var request = URLRequest(url: URL(string: "https://api.example.com/users")!)
11request.httpBody = jsonData
12request.httpMethod = "POST"
13request.setValue("application/json", forHTTPHeaderField: "Content-Type")
Dictionary Back to Codable
1let dict: [String: Any] = [
2 "name": "Charlie",
3 "age": 25,
4 "email": "[email protected]"
5]
6
7let data = try JSONSerialization.data(withJSONObject: dict)
8let user = try JSONDecoder().decode(User.self, from: data)
9print(user.name) // Charlie
Common Pitfalls
Date encoding defaults to Double: Without setting dateEncodingStrategy, JSONEncoder encodes Date as a TimeInterval (seconds since 1970). Set .iso8601 or .formatted for readable dates in your dictionary.
Any type is not Codable: You cannot encode [String: Any] with JSONEncoder because Any does not conform to Codable. Use JSONSerialization for untyped dictionaries.
Enums without raw values: Enums with associated values need custom Codable conformance. Without it, encoding fails at runtime.
Optional fields are omitted: By default, JSONEncoder omits nil optional properties. The resulting dictionary will not have keys for nil values. If you need explicit null, use custom encoding.
Float/Double precision: JSON serialization may introduce floating-point precision artifacts. 3.14 might become 3.1400000000000001 in the dictionary. Use Decimal for exact values.
Summary
Encode to JSON with JSONEncoder, then parse to [String: Any] with JSONSerialization
Create an Encodable extension with asDictionary() for reusable conversion
Custom CodingKeys control the dictionary key names (e.g., camelCase to snake_case)
Set dateEncodingStrategy on JSONEncoder to control how dates appear in the dictionary
To go from dictionary back to Codable, serialize with JSONSerialization then decode with JSONDecoder