How to Serialize a Dictionary to JSON in Swift

How to Serialize a Dictionary to JSON in Swift

Written by

in

Quick answer: Use JSONSerialization.data(withJSONObject:options:) for dynamic [String: Any] dictionaries, passing .prettyPrinted if you need formatted output. For type-safe dictionaries conforming to Encodable, pass the dictionary directly into JSONEncoder().encode(dictionary). Convert the resulting Data to a displayable string using String(data: jsonData, encoding: .utf8).

Why this happens

Converting a Swift dictionary into JSON usually encounters two main friction points: misunderstanding the debugger representation and violating JSON format constraints.

First, Swift prints dictionaries in the console and Playgrounds using bracket notation—such as ["key": "value"]—which looks like an array to developers expecting curly braces ({ "key": "value" }). The bracket syntax in Xcode is purely the CustomStringConvertible description of Swift dictionaries, not raw JSON.

Second, JSON specifications dictate that top-level key-value mappings must use string keys. If your dictionary uses integer keys (such as [Int: String]), standard JSONSerialization fails validation via JSONSerialization.isValidJSONObject(_:) and throws an NSInvalidArgumentException at runtime. In addition, all values nested in the dictionary must conform to valid JSON types: String, NSNumber (including Int, Double, Bool), Array, Dictionary, or NSNull.

How to fix it

1. Convert dynamic dictionaries with JSONSerialization

If your dictionary has mixed value types (typed as [String: Any]), use JSONSerialization. Ensure top-level keys are strings. Wrap the call in a do-catch block to handle encoding failures cleanly.

import Foundation

let payload: [String: Any] = [
    "userId": 1024,
    "name": "Alex",
    "roles": ["admin", "editor"],
    "isActive": true
]

// Validate the structure before serializing (optional but safe)
guard JSONSerialization.isValidJSONObject(payload) else {
    fatalError("Payload contains values that cannot be serialized to JSON.")
}

do {
    let jsonData = try JSONSerialization.data(withJSONObject: payload, options: .prettyPrinted)

    // Convert the data buffer to a readable UTF-8 string
    if let jsonString = String(data: jsonData, encoding: .utf8) {
        print(jsonString)
    }
} catch {
    print("JSON serialization failed: \(error.localizedDescription)")
}

The output will be correctly formatted with curly braces:

{
  "userId" : 1024,
  "name" : "Alex",
  "roles" : [
    "admin",
    "editor"
  ],
  "isActive" : true
}

2. Convert typed dictionaries using JSONEncoder

When your keys and values conform to Encodable (such as [String: String], [String: Int], or dictionaries holding custom structs), JSONEncoder provides compile-time safety and does not require casting to Any.

import Foundation

let scores: [String: Int] = [
    "TeamA": 42,
    "TeamB": 38,
    "TeamC": 55
]

let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]

do {
    let jsonData = try encoder.encode(scores)
    if let jsonString = String(data: jsonData, encoding: .utf8) {
        print(jsonString)
    }
} catch {
    print("Encoding error: \(error.localizedDescription)")
}

JSONEncoder avoids the legacy Objective-C bridging overhead of JSONSerialization and natively respects options like .sortedKeys.

3. Handle non-string keys (such as [Int: String])

JSON strictly requires object keys to be strings. If you have an [Int: String] or [Int: Any] dictionary, JSONSerialization will throw an exception. Map integer keys to strings before serializing:

import Foundation

let numericKeyDict: [Int: String] = [
    1: "First",
    2: "Second",
    3: "Third"
]

// Transform Int keys to String keys
let stringKeyDict = Dictionary(uniqueKeysWithValues: numericKeyDict.map {
    (String($0.key), $0.value)
})

do {
    let data = try JSONSerialization.data(withJSONObject: stringKeyDict, options: [.sortedKeys])
    if let output = String(data: data, encoding: .utf8) {
        print(output)
        // Output: {"1":"First","2":"Second","3":"Third"}
    }
} catch {
    print("Serialization error: \(error)")
}

Alternatively, modern JSONEncoder automatically stringifies Int keys into string dictionary keys during encoding without requiring manual mapping:

let intKeyMap: [Int: String] = [101: "Approved", 404: "Not Found"]
let encoder = JSONEncoder()
if let data = try? encoder.encode(intKeyMap), let text = String(data: data, encoding: .utf8) {
    print(text)
    // Output: {"101":"Approved","404":"Not Found"}
}

4. Create a reusable conversion extension

For cleaner call sites across your networking layer or test suites, encapsulate the logic in a generic extension on Dictionary.

import Foundation

extension Dictionary {
    /// Serializes the dictionary into JSON Data.
    func toJSONData(prettyPrinted: Bool = false) throws -> Data {
        let options: JSONSerialization.WritingOptions = prettyPrinted ? [.prettyPrinted] : []
        guard JSONSerialization.isValidJSONObject(self) else {
            throw NSError(
                domain: "JSONError",
                code: 1001,
                userInfo: [NSLocalizedDescriptionKey: "Invalid JSON object structure"]
            )
        }
        return try JSONSerialization.data(withJSONObject: self, options: options)
    }

    /// Serializes the dictionary directly into a UTF-8 JSON String.
    func toJSONString(prettyPrinted: Bool = false) -> String? {
        guard let data = try? toJSONData(prettyPrinted: prettyPrinted) else {
            return nil
        }
        return String(data: data, encoding: .utf8)
    }
}

// Usage:
let requestBody: [String: Any] = ["query": "swift", "page": 1]
if let jsonText = requestBody.toJSONString(prettyPrinted: true) {
    print(jsonText)
}

How to prevent it

  • Check your object with JSONSerialization.isValidJSONObject(_:) before calling .data(withJSONObject:) whenever dealing with dynamic untyped data.
  • Prefer Codable structs over loose [String: Any] dictionaries whenever data schemas are known at compile time.
  • Never pass non-standard floating point values like Double.nan or Double.infinity into a JSON dictionary; standard JSON does not support them.
  • Use .utf8 rather than .ascii when constructing strings from Data to avoid dropping non-Latin characters or emojis.

FAQ

Why does my dictionary print with square brackets instead of curly braces?

Swift formats dictionaries as [Key: Value] in print() statements and LLDB for language consistency. This is purely visual; once you serialize the dictionary with JSONSerialization or JSONEncoder, the resulting byte stream uses standard JSON curly braces { ... }.

How do I write JSON Data directly to a POST URLRequest?

Assign the returned Data directly to urlRequest.httpBody and set the Content-Type header to application/json. You do not need to convert the data into a string first.

When should I use JSONEncoder instead of JSONSerialization?

Use JSONEncoder when your models or dictionary values are strongly typed and conform to Encodable. Use JSONSerialization when handling dynamic dictionaries ([String: Any]) where types vary at runtime and cannot be mapped to static structs.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.