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
Codablestructs over loose[String: Any]dictionaries whenever data schemas are known at compile time. - Never pass non-standard floating point values like
Double.nanorDouble.infinityinto a JSON dictionary; standard JSON does not support them. - Use
.utf8rather than.asciiwhen constructing strings fromDatato 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.
Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.
