Introduction
When a JSON object contains a field with an arbitrary key-value dictionary (like "metadata": {"color": "red", "size": 42}), Swift's Decodable protocol cannot decode it directly into [String: Any] because Any does not conform to Decodable. The solutions are: define the dictionary as [String: AnyCodable] using a custom wrapper type, use a custom init(from decoder:) to manually decode the container, or define a concrete struct that matches the dictionary's known shape. For truly dynamic JSON, create an AnyCodable enum that handles all JSON value types.
The Problem
1struct User: Decodable {
2 let name: String
3 let metadata: [String: Any] // ❌ Error: Type 'Any' does not conform to 'Decodable'
4}
5
6// JSON:
7// {
8// "name": "Alice",
9// "metadata": {
10// "color": "red",
11// "age": 30,
12// "active": true
13// }
14// }
[String: Any] does not conform to Decodable because Swift's type system requires every type to have a known decoding strategy. Any is not a concrete type, so the compiler cannot generate a decoder for it.
Fix 1: Known Structure — Define a Struct
1// If you know the dictionary's structure, define it explicitly
2struct Metadata: Decodable {
3 let color: String
4 let age: Int
5 let active: Bool
6}
7
8struct User: Decodable {
9 let name: String
10 let metadata: Metadata // ✅ Concrete type
11}
12
13let json = """
14{
15 "name": "Alice",
16 "metadata": {"color": "red", "age": 30, "active": true}
17}
18""".data(using: .utf8)!
19
20let user = try JSONDecoder().decode(User.self, from: json)
21print(user.metadata.color) // "red"
This is the preferred approach when the dictionary schema is known and stable. It provides full type safety and compiler-checked property access.
Fix 2: AnyCodable Enum for Dynamic Values
1enum JSONValue: Decodable {
2 case string(String)
3 case int(Int)
4 case double(Double)
5 case bool(Bool)
6 case array([JSONValue])
7 case dictionary([String: JSONValue])
8 case null
9
10 init(from decoder: Decoder) throws {
11 let container = try decoder.singleValueContainer()
12
13 if let value = try? container.decode(String.self) {
14 self = .string(value)
15 } else if let value = try? container.decode(Int.self) {
16 self = .int(value)
17 } else if let value = try? container.decode(Double.self) {
18 self = .double(value)
19 } else if let value = try? container.decode(Bool.self) {
20 self = .bool(value)
21 } else if let value = try? container.decode([JSONValue].self) {
22 self = .array(value)
23 } else if let value = try? container.decode([String: JSONValue].self) {
24 self = .dictionary(value)
25 } else if container.decodeNil() {
26 self = .null
27 } else {
28 throw DecodingError.dataCorrupted(
29 .init(codingPath: decoder.codingPath,
30 debugDescription: "Unsupported JSON value"))
31 }
32 }
33}
34
35struct User: Decodable {
36 let name: String
37 let metadata: [String: JSONValue] // ✅ Handles any JSON value type
38}
39
40let user = try JSONDecoder().decode(User.self, from: json)
41
42// Access values with pattern matching
43if case .string(let color) = user.metadata["color"] {
44 print(color) // "red"
45}
46if case .int(let age) = user.metadata["age"] {
47 print(age) // 30
48}
The JSONValue enum covers all JSON types. It is fully Decodable and handles nested structures recursively.
Fix 3: Custom init(from decoder:) with JSONSerialization
1struct User: Decodable {
2 let name: String
3 let metadata: [String: Any]
4
5 enum CodingKeys: String, CodingKey {
6 case name, metadata
7 }
8
9 init(from decoder: Decoder) throws {
10 let container = try decoder.container(keyedBy: CodingKeys.self)
11 name = try container.decode(String.self, forKey: .name)
12
13 // Decode metadata as a raw JSON object
14 let metadataDecoder = try container.superDecoder(forKey: .metadata)
15 let metadataContainer = try metadataDecoder.singleValueContainer()
16
17 // Use JSONSerialization via a workaround
18 if let data = try? JSONEncoder().encode(
19 container.decode(AnyCodableDict.self, forKey: .metadata)
20 ) {
21 metadata = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] ?? [:]
22 } else {
23 metadata = [:]
24 }
25 }
26}
This approach is more complex and less type-safe. Prefer the JSONValue enum approach for cleaner code.
Fix 4: Using DynamicCodingKeys
1struct User: Decodable {
2 let name: String
3 let metadata: [String: String] // Works when all values are the same type
4
5 enum CodingKeys: String, CodingKey {
6 case name, metadata
7 }
8
9 struct DynamicCodingKeys: CodingKey {
10 var stringValue: String
11 init?(stringValue: String) { self.stringValue = stringValue }
12 var intValue: Int? { nil }
13 init?(intValue: Int) { nil }
14 }
15
16 init(from decoder: Decoder) throws {
17 let container = try decoder.container(keyedBy: CodingKeys.self)
18 name = try container.decode(String.self, forKey: .name)
19
20 let metaContainer = try container.nestedContainer(
21 keyedBy: DynamicCodingKeys.self, forKey: .metadata)
22
23 var dict = [String: String]()
24 for key in metaContainer.allKeys {
25 if let value = try? metaContainer.decode(String.self, forKey: key) {
26 dict[key.stringValue] = value
27 }
28 }
29 metadata = dict
30 }
31}
DynamicCodingKeys allows decoding a JSON object with unknown keys. This is useful when all values are the same type (e.g., all strings or all integers).
Fix 5: Codable Dictionary with Concrete Value Types
1// When values are mixed but from a known set of types
2struct FlexibleValue: Decodable {
3 let stringValue: String?
4 let intValue: Int?
5 let boolValue: Bool?
6
7 init(from decoder: Decoder) throws {
8 let container = try decoder.singleValueContainer()
9 stringValue = try? container.decode(String.self)
10 intValue = try? container.decode(Int.self)
11 boolValue = try? container.decode(Bool.self)
12 }
13
14 var value: Any {
15 if let s = stringValue { return s }
16 if let i = intValue { return i }
17 if let b = boolValue { return b }
18 return NSNull()
19 }
20}
21
22struct User: Decodable {
23 let name: String
24 let metadata: [String: FlexibleValue]
25}
26
27let user = try JSONDecoder().decode(User.self, from: json)
28print(user.metadata["color"]?.stringValue) // Optional("red")
29print(user.metadata["age"]?.intValue) // Optional(30)
Common Pitfalls
Trying to decode [String: Any] directly: Any does not conform to Decodable or Codable. Swift requires concrete types for automatic synthesis. Use JSONValue enum, a typed dictionary ([String: String]), or a custom init(from:).
Order of type checks in JSONValue: When decoding, check Bool before Int because JSON true/false can be decoded as Int (1/0) by some decoders. Similarly, check Int before Double to preserve integer precision.
Losing type information with [String: Any]: Even if you successfully decode to [String: Any], you lose compile-time type safety and must cast values at every access point. Prefer typed structs or the JSONValue enum for safer code.
Forgetting CodingKeys in custom init: When implementing init(from decoder:), you must define CodingKeys enum (or use DynamicCodingKeys) and decode each property manually. Missing a property causes it to be nil or use its default value silently.
Not handling nested dictionaries: If metadata contains nested objects ("address": {"city": "NYC"}), a flat [String: String] decoder ignores them. Use the recursive JSONValue enum to handle arbitrary nesting depth.
Summary
[String: Any] does not conform to Decodable — you cannot use it directly
Define a concrete struct when the dictionary schema is known (safest approach)
Create a JSONValue enum with cases for each JSON type for fully dynamic dictionaries
Use DynamicCodingKeys to decode objects with unknown but same-typed values
Check Bool before Int and Int before Double when detecting JSON value types
Prefer typed approaches over [String: Any] for compile-time safety