SwiftyJSON: subscript chains that report errors, and three install paths pinned to 4.0
The better way to deal with JSON data in Swift.
At a glance
- What is it?
- SwiftyJSON is an MIT licensed Swift library that wraps JSON data so you read it with subscripts and get a named error instead of a trap. The API is small and forgiving, while the tooling still resolves to the 4.x line even though the requirements ask for Swift 6.0.
- Who is it for?
- SwiftyJSON fits codebases holding loosely typed JSON that want a printable reason at the point a lookup fails, without writing unwraps. It does not fit teams standardising on Swift Codable, and the copy paste snippets will not give you the 5.0.2 tag.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 88 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- SwiftyJSON/SwiftyJSON22,919 ★ · -24
| Repository | 2026-09-21 | 2026-11-13 |
|---|---|---|
| SwiftyJSON/SwiftyJSON | 22.9k | 22.9k |
-24 Stars in the last 53 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Chained subscripts that wrap the optional for you
The library's central move is reading a document with brackets instead of nested casts. Parse once, and every bracket hands back another JSON rather than a raw Any:
let json = try? JSON(data: dataFromNetworking)
if let userName = json[0]["user"]["name"].string {
//Now you got your value
}The `.string` accessor still produces an Optional String, so the compiler keeps forcing you to deal with absence, and the same accessor works whether the segment underneath was an array index or a dictionary key. Parsing has three documented entries: `JSON(data: dataFromNetworking)` for bytes, `JSON(jsonObject)` for a value you already hold, and a string pushed through `jsonString.data(using: .utf8, allowLossyConversion: false)` before it reaches JSON(data:). What you trade away is compile time checking. A field the server renames no longer fails your build, it becomes an empty JSON at runtime and your optional binding quietly takes the else branch, so a schema mistake turns into a shipping bug rather than a compiler error.
Non optional getters drop the error on the floor
Every accessor ships in two forms. `.string` and `.double` return optionals and keep the failure attached, while `.stringValue` and `.arrayValue` return non optional values, which is what the array mapping example uses:
let arrayNames = json["users"].arrayValue.map {$0["name"].stringValue}Choose the non optional form and a missing key stops being visible. `stringValue` hands back an empty String, and nothing at the call site separates a field that was absent from one that held an empty value. The reason lives on the result of the lookup, not on the parse, so a chain of non optional getters never surfaces it:
let json = try? JSON(data: dataFromNetworking)
let result = json[999999]["wrong_key"]["wrong_name"]
if let userName = result.string {
//Calm down, take it easy, the ".string" property still produces the correct Optional String type with safety
} else {
//Print the error
print(result.error)
}That last block is the pattern to copy. If your code reads `.stringValue` everywhere for convenience, a renamed or removed field arrives as an empty string and nothing in your logs says the key was wrong.
SwiftyJSONError names the six ways a lookup fails
The 4.x line replaced loose error constants with an enum named SwiftyJSONError carrying unsupportedType, indexOutOfBounds, elementTooDeep, wrongType, notExist and invalidJSON, and the old ErrorDomain is now reached as SwiftyJSONError.errorDomain. The previous error types are deprecated in 4.x and will be removed in a future release, so comparisons written against the old names need revisiting before you upgrade again. Out of range subscripts no longer trap:
let json = JSON(["name", "age"])
if let name = json[999].string {
// Do something you want
} else {
print(json[999].error!) // "Array[999] is out of bounds"
}A missing dictionary key reports "Dictionary["address"] does not exist", and `JSON(12345)` indexed at 0 prints "Array[0] failure, It is not an array" both as the value and as its error, so a scalar read through a container subscript degrades into printable text. That is the behaviour the project set out to replace: with a plain subscript an array index could crash the app, a missing dictionary key was assigned nil with no reason, and a non container raised an unrecognised selector exception. You gain a reason at the point of failure. You do not gain a trace back to the server change that caused it.
Path subscripts mix indexes and string keys in one array
The subscript takes a heterogeneous array of indexes and keys, and three spellings of the same walk are equivalent:
// Getting a string using a path to the element
let path: [JSONSubscriptType] = [1,"list",2,"name"]
let name = json[path].string
// Just the same
let name = json[1]["list"][2]["name"].string
// Alternatively
let name = json[1,"list",2,"name"].stringThe README also shows `json[].string`, and any array of JSONSubscriptType you assemble yourself works the same way, so paths can be built at runtime from configuration instead of written out. The cost is diagnostic. A path that runs into a wrong type or a missing segment gives you a single JSON carrying a single error, and nothing in the result names which element of the path broke. elementTooDeep is the one case in the enum that separates a depth problem from a plain notExist, so a long generated path is debugged by bisecting it by hand.
Loop iteration hands array indexes over as strings
Iterating a JSON value yields `(String, JSON)` pairs, and for an array the first element is the index. The usage section states the awkward part directly: the index is the string value of 0..<json.count. For a dictionary that String is the key, which is what you want. For an array you are holding a String where an Int belongs, so comparing the index against json.count, slicing a Swift array with it, or passing it to a typed API needs a conversion at every use, and a loop that indexes back into the JSON has to turn the String into an Int before it compiles as a subscript. The loop signature does not flag this, because the same tuple type serves the dictionary and array cases and the compiler has no way to narrow it.
Carthage and CocoaPods snippets both resolve to the 4.x line
Four installation routes are offered, and both dependency manager snippets carry the same old constraint. CocoaPods:
platform :ios, '8.0'
use_frameworks!
target 'MyApp' do
pod 'SwiftyJSON', '~> 4.0'
endCarthage takes the identical pin in the Cartfile:
github "SwiftyJSON/SwiftyJSON" ~> 4.0Copy either one and your resolver picks the newest 4.x rather than 5.0.2, the tag published 2024-03-27, whose predecessor 5.0.0 arrived 2019-04-02 and before that 4.2.0 on 2018-09-25. Reaching 5.x means editing the constraint yourself. The project itself is not idle: the last push is dated 2026-08-18, well after the newest tag, so commits are landing while releases are not. That gap is the practical trap, since copying the documented line quietly freezes you a major version back from the code people are actually reading on the master branch.
The Package.swift manifest and the Requirements block disagree on the floor
Swift Package Manager takes a manifest rather than a constraint file:
// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "YOUR_PROJECT_NAME",
dependencies: [
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "4.0.0"),
]
)Then run `swift build` whenever you get prepared, which is the only build command given here. The Requirements block, in the same section, asks for iOS 8.0+, macOS 10.10+, tvOS 9.0+ and watchOS 2.0+, alongside Xcode 16+ and Swift 6.0+. A floor of iOS 8.0 and macOS 10.10 next to Xcode 16 is two generations of toolchain in one list, and the manifest still declares `// swift-tools-version:4.0`, the tools version from the format's introduction rather than the compiler you build with. The manual heading goes further back again, to iOS 7+ and OS X 10.9+, while the CocoaPods and Carthage headings say iOS 8+ and OS X 10.9+. Three minimums are named and none of them says which one your toolchain actually enforces, so a wrong guess surfaces as a build failure far from the line that caused it.
Carthage wants two Xcode edits, the manual path wants one file
Outside the package managers, the manual route splits by workspace type: drag SwiftyJSON.swift into the project tree for a project, or include the whole SwiftyJSON.xcodeproj for a workspace. Carthage carries its own manual work, because the Cartfile entry alone is not enough. SwiftyJSON.framework has to be added to the target's Linked Frameworks and Libraries section and included in the Carthage framework copying build phase, or nothing links at run time. Neither path fetches updates for you, and dragging a single file gives you the source without a framework of its own, so the `import SwiftyJSON` line that opens the Usage section only lines up when the library is a separate module. When a snippet misbehaves, the tree tells you where to look instead of guessing: Source/, Tests/, SwiftyJSON.podspec, Package.swift, CHANGELOG.md, .swiftlint.yml, .hound.yml, and an Example/ directory holding an Xcode project and a Playground.playground.
Editorial conclusion
SwiftyJSON fits codebases holding loosely typed JSON that want a printable reason at the point a lookup fails, without writing unwraps. It does not fit teams standardising on Swift Codable, and the copy paste snippets will not give you the 5.0.2 tag. Before adopting it, check the platform floor your own toolchain enforces against the conflicting minimums in the Requirements section, raise the constraint to 5.x by hand, and decide early whether you read .string with its error or the non optional .stringValue, because that choice decides whether a missing field ever reaches your logs.
Frequently asked questions
What is JSON and why does SwiftyJSON build its whole API around it?
JSON is the interchange format the library parses and wraps. You hand it bytes with JSON(data:) or a value you already hold with JSON(jsonObject), then read fields through subscripts such as json[0]["user"]["name"] and accessors like .string.
How do I convert a JSON file to readable text using SwiftyJSON?
Turn the string into UTF-8 data first with jsonString.data(using: .utf8, allowLossyConversion: false), wrap that data in JSON, then read values one path at a time through subscripts and the .string accessor.
How can SwiftyJSON convert a string into JSON?
The documented initializers take data rather than a raw string, so the conversion goes through data(using: .utf8, allowLossyConversion: false) and the result goes straight into JSON(data:).
How do I obtain a JSON file for SwiftyJSON to parse?
The initialization examples work with bytes the app already holds, named dataFromNetworking, or data built from a string. The library fetches nothing itself; separate sections cover pairing it with Alamofire and with Moya.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/swiftyjson-swiftyjson)Star history chart for your README
[](https://hysenlabs.com/projects/swiftyjson-swiftyjson)Was this useful?