PMJSON
Pure Swift JSON encoding/decoding library
Install / Use
/learn @postmates/PMJSONREADME
Now Archived and Forked
PMJSON will not be maintained in this repository going forward. Please use, create issues on, and make PRs to the fork of PMJSON located here.
PMJSON
PMJSON provides a pure-Swift strongly-typed JSON encoder/decoder as well as a set of convenience methods for converting to/from Foundation objects and for decoding JSON structures.
The entire JSON encoder/decoder can be used without Foundation, by removing the files ObjectiveC.swift and DecimalNumber.swift from the project. The only dependency the rest of the project has is on Darwin, for strtod() and strtoll(). The file ObjectiveC.swift adds convenience methods for translating between JSON values and Foundation objects as well as decoding from a Data, and DecimalNumber.swift adds convenience accessors for converting values into NSDecimalNumber.
Usage
Before diving into the details, here's a simple example of writing a decoder for a struct. There are a few different options for how to deal with malformed data (e.g. whether to ignore values of wrong types, and whether to try and coerce non-string values to strings or vice versa), but the following example will be fairly strict and throw an error for incorrectly-typed values:
struct Address {
var streetLine1: String
var streetLine2: String?
var city: String
var state: String?
var postalCode: String
var country: String?
init(json: JSON) throws {
streetLine1 = try json.getString("street_line1")
streetLine2 = try json.getStringOrNil("street_line2")
city = try json.getString("city")
state = try json.getStringOrNil("state")
postalCode = try json.toString("postal_code") // coerce numbers to strings
country = try json.getStringOrNil("country")
}
}
And here's an example of decoding a nested array of values:
struct Person {
var firstName: String
var lastName: String? // some people don't have last names
var age: Int
var addresses: [Address]
init(json: JSON) throws {
firstName = try json.getString("firstName")
lastName = try json.getStringOrNil("lastName")
age = try json.getInt("age")
addresses = try json.mapArray("addresses", Address.init(json:))
}
}
If you don't want to deal with errors and just want to handle optionals, you can do that too:
struct Config {
var name: String?
var doThatThing: Bool
var maxRetries: Int
init(json: JSON) {
name = json["name"]?.string
doThatThing = json["doThatThing"]?.bool ?? false
maxRetries = json["maxRetries"]?.int ?? 10
}
}
This library also provides support for Swift.Encoder and Swift.Decoder. See this section for details.
Parsing
The JSON decoder is split into separate parser and decoder stages. The parser consums any sequence of unicode scalars, and produces a sequence of JSON "events" (similar to a SAX XML parser). The decoder accepts a sequence of JSON events and produces a JSON value. This architecture is designed such that you can use just the parser alone in order to decode directly to your own data structures and bypass the JSON representation entirely if desired. However, most clients are expected to use both components, and this is exposed via a simple method JSON.decode(_:options:).
Parsing a JSON string into a JSON value is as simple as:
let json = try JSON.decode(jsonString)
Any errors in the JSON parser are represented as JSONParserError values and are thrown from the decode() method. The error contains the precise line and column of the error, and a code that describes the problem.
A convenience method is also provided for decoding from a Data containing data encoded as UTF-8, UTF-16, or UTF-32:
let json = try JSON.decode(data)
Encoding a JSON value is also simple:
let jsonString = JSON.encodeAsString(json)
You can also encode directly to any TextOutputStream:
JSON.encode(json, toStream: &output)
And, again, a convenience method is provided for working with Data:
let data = JSON.encodeAsData(json)
JSON Streams
PMJSON supports parsing JSON streams, which are multiple top-level JSON values with optional whitespace delimiters (such as {"a": 1}{"a": 2}). The easiest way to use this is with JSON.decodeStream(_:) which returns a lazy sequence of JSONStreamValues, which contain either a JSON value or a JSONParserError error. You can also use JSONParsers and JSONDecoders directly for more fine-grained control over streaming.
JSONParser and JSONDecoder
As mentioned above, the JSON decoder is split into separate parser and decoder stages. JSONParser is the parser stage, and it wraps any sequence of UnicodeScalars, and itself is a sequence of JSONEvents. A JSONEvent is a single step of JSON parsing, such as .objectStart when a { is encountered, or .stringValue(_) when a "string" is encountered. You can use JSONParser directly to emit a stream of events if you want to do any kind of lazy processing of JSON (such as if you're dealing with a single massive JSON blob and don't want to decode the whole thing into memory at once).
Similarly, JSONDecoder is the decoder stage. It wraps a sequence of JSONEvents, and decodes that sequence into a proper JSON value. The wrapped sequence must also conform to a separate protocol JSONEventIterator that provides line/column information, which are used when emitting errors. You can use JSONDecoder directly if you want to wrap a sequence of events other than JSONParser, or if you want a different interface to JSON stream decoding than JSONStreamDecoder provides.
Because of this split nature, you can easily provide your own event stream, or your own decoding stage. Or you can do things like wrap JSONParser in an adaptor that modfiies the events before passing them to the decoder (which may be more efficient than converting the resulting JSON value).
Accessors
Besides encoding/decoding, this library also provides a comprehensive suite of accessors for getting data out of JSON values. There are 4 types of basic accessors provided:
- Basic property accessors named after types such as
.string. These accessors return the underlying value if it matches the type, ornilif the value is not the right type. For example,.stringreturnsString?. These accessors do not convert between types, e.g.JSON.Int64(42).stringreturnsnil. - Property accessors beginning with the word
as, such as.asString. These accessors also return an optional value, but they convert between types if it makes sense to do so. For example,JSON.Int64(42).asStringreturns"42". - Methods beginnning with
get, such asgetString(). These methods return non-optional values, and throwJSONErrors if the value's type does not match. These methods do not convert between types, e.g.try JSON.Int64(42).getString()throws an error. For every method of this type, there's also a variant ending inOrNil, such asgetStringOrNil(), which does return an optional. These methods only returnnilif the value isnull, otherwise they throw an error. - Methods beginning with
to, such astoString(). These are just like thegetmethods except they convert between types when appropriate, using the same rules that theasmethods do, e.g.try JSON.Int64(42).toString()returns"42". Like thegetmethods, there are also variants ending inOrNil.
JSON also provides both keyed and indexed subscript operators that return a JSON?, and are always safe to call (even with out-of-bounds indexes). And it provides 2 kinds of subscripting accessors:
- For every basic
getaccessor, there's a variant that takes a key or an index. These are equivalent to subscripting the receiver and invoking thegetaccessor on the result, except they produce better errors (and they handle missing keys/out-of-bounds indexes properly). For example,getString("key")orgetString(index). TheOrNilvariants also returnnilif the key doesn't exist or the index is out-of-bounds. - Similarly, there are subscripting equivalents for the
toaccessors as well.
And finally, the getObject() and getArray() accessors provide variants that take a closure. These variants are recommended over the basic accessors as they produce better errors. For example, given the following JSON:
{
"object": {
"elements": [
{
"name": null
}
]
}
}
And the following code:
try json.getObject("object").getArray("elements").getObject(0).getString("name")
The error thrown by this code will have the description "name: expected string, found null".
But given the following equivalent code:
try json.getObject("object", { try $0.getArray("elements", { try $0.getObject(0, { try $0.getString("name") }) }) })
The error thrown by this code will have the description "object.elements[0].name: expected string, found null".
All of these accessors are also available on the JSONObject type (which is the type that represents an object).
The last code snippet above looks very verbose, b
Related Skills
node-connect
347.0kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
107.8kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
347.0kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
347.0kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
