Swift
JSON
Decodable
Property
Dictionary

How to decode a property with type of JSON dictionary in Swift 45 decodable protocol

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

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

swift
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

swift
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

swift
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

swift
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

swift
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

swift
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

Course illustration
Course illustration

All Rights Reserved.