swift-sion
OBSOLETED byswift-sion can do all what swift-json
can do plus:
-
Handles SION, which is a "JSON++" with support for more data types.
-
Conversion betwen:
- JSON
- Property List
- MsgPack
- YAML (output only)
swift-json
Handle JSON safely, fast, and expressively. Completely rewritten from ground up for Swift 4 and Swift Package Manager.
Synopsis
import JSON
let json:JSON = ["swift":["safe","fast","expressive"]]
Description
This module is a lot like SwiftyJSON in functionality. It wraps JSONSerialization nicely and intuitively. But it differs in how to do so.
- SwiftyJSON's
JSON
isstruct
.JSON
of this module isenum
- SwiftyJSON keeps the output of
JSONSerialization.jsonObject
in its stored property and convert its value runtime.JSON
of this module is static. Definitely Swiftier. - SwiftyJSON's
JSON.swift
is over 1,500 lines while that of this module is less than 400 (as of this writing). Since it is so compact you can use it without building framework.
Initialization
You can build JSON directly as a literal…
let json:JSON = [
"null": nil,
"bool": true,
"int": -42,
"double": 42.195,
"string": "漢字、カタカナ、ひらがなと\"引用符\"の入ったstring😇",
"array": [nil, true, 1, "one", [1], ["one":1]],
"object": [
"null":nil, "bool":false, "number":0, "string":"" ,"array":[], "object":[:]
],
"url":"https://github.com/dankogai/"
]
…or String…
let str = """
{
"null": null,
"bool": true,
"int": -42,
"double": 42.195,
"string": "漢字、カタカナ、ひらがなと\\"引用符\\"の入ったstring😇",
"array": [null, true, 1, "one", [1], {"one":1}],
"object": {
"null":null, "bool":false, "number":0, "string":"" ,"array":[], "object":{}
},
"url":"https://github.com/dankogai/"
}
"""
JSON(string:str)
…or a content of the URL…
JSON(urlString:"https://api.github.com")
…or by decoding Codable
data…
import Foundation
struct Point:Hashable, Codable { let (x, y):(Int, Int) }
var data = try JSONEncoder().encode(Point(x:3, y:4))
try JSONDecoder().decode(JSON.self, from:data)
Conversion
once you have the JSON object, converting to other formats is simple.
to JSON string, all you need is stringify it. .description or "(json)" would be enough.
json.description
"\(json)" // JSON is CustomStringConvertible
If you need Data
, simply call .data
.
json.data
If you want to feed it (back) to Foundation
framework, call .jsonObject
let json4plist = json.pick{ !$0.isNull } // remove null
let plistData = try PropertyListSerialization.data (
fromPropertyList:json4plist.jsonObject,
format:.xml,
options:0
)
print(String(data:plistData, encoding:.utf8)!)
Manipulation
a blank JSON Array is as simple as:
var json = JSON([])
and you can assign elements like an ordinary array
json[0] = nil
json[1] = true
json[2] = 1
note RHS literals are NOT nil
, true
and 1
but .Null
, .Bool(true)
and .Number(1)
. Therefore this does NOT work
let one = "one"
json[3] = one // error: cannot assign value of type 'String' to type 'JSON'
In which case you do this instead.
json[3].string = one
They are all getters and setters.
json[1].bool = true
json[2].number = 1
json[3].string = "one"
json[4].array = [1]
json[5].object = ["one":1]
As a getter they are optional which returns nil
when the type mismaches.
json[1].bool // Optional(true)
json[1].number // nil
Therefore, you can mutate like so:
json[2].number! += 1 // now 2
json[3].string!.removeLast() // now "on"
json[4].array!.append(2) // now [1, 2]
json[5].object!["two"] = 2 // now ["one":1,"two":2]
When you assign values to JSON array with an out-of-bound index, it is automatically streched with unassigned elements set to null
, just like an ECMAScript Array
json[10] = false // json[6...9] are null
As you may have guessed by now, a blank JSON object(dictionary) is:
json = JSON([:])
And manipulate intuitively like so.
json["null"] = nil // not null
json["bool"] = false
json["number"] = 0
json["string"] = ""
json["array"] = []
json["object"] = [:] // not {}
deep traversal
JSON
is a recursive data type. For recursive data types, you need a recursive method that traverses the data deep down. For that purpuse, JSON
offers .pick
and .walk
.
.pick
is a ".deepFilter
" that filters recursively. You've already seen it above. It takes a filter function of type (JSON)->Bool
. That function is applied to all leaf values of the tree and leaves that do not meet the predicate are pruned.
// because property list does not accept null
let json4plist = json.pick{ !$0.isNull }
.walk
is a deepMap
that transforms recursively. This one is a little harder because you have to consider what to do on node and leaves separately. To make your life easier three different versions of .walk
are provided. The first one just takes a leaf node.
// square all numbers and leave anything else
JSON([0,[1,[2,3,[4,5,6]]], true]).walk {
guard let n = $0.number else { return $0 }
return JSON(n * n)
}
The second forms just takes a node. Instead of explaining it, let me show you how .pick
is implemented by extending JSON
with .select
that does exactly the same as .pick
.
extension JSON {
func select(picker:(JSON)->Bool)->JSON {
return self.walk{ node, pairs, depth in
switch node.type {
case .array:
return .Array(pairs.map{ $0.1 }.filter({ picker($0) }) )
case .object:
var o = [Key:Value]()
pairs.filter{ picker($0.1) }.forEach{ o[$0.0.key!] = $0.1 }
return .Object(o)
default:
return .Error(.notIterable(node.type))
}
}
}
}
And the last form takes both. Unlike the previous ones this one can return other than JSON
. Here is a quick and dirty .yaml
that emits a YAML.
extension JSON {
var yaml:String {
return self.walk(depth:0, collect:{ node, pairs, depth in
let indent = Swift.String(repeating:" ", count:depth)
var result = ""
switch node.type {
case .array:
guard !pairs.isEmpty else { return "[]"}
result = pairs.map{ "- " + $0.1}.map{indent + $0}.joined(separator: "\n")
case .object:
guard !pairs.isEmpty else { return "{}"}
result = pairs.sorted{ $0.0.key! < $1.0.key! }.map{
let k = $0.0.key!
let q = k.rangeOfCharacter(from: .newlines) != nil
return (q ? k.debugDescription : k) + ": " + $0.1
}.map{indent + $0}.joined(separator: "\n")
default:
break // never reaches here
}
return "\n" + result
},visit:{
if $0.isNull { return "~" }
if let s = $0.string {
return s.rangeOfCharacter(from: .newlines) == nil ? s : s.debugDescription
}
return $0.description
})
}
}
Protocol Conformance
JSON
isEquatable
so you can check if two JSONs are the same.
JSON(string:foo) == JSON(urlString:"https://example.com/whereever")
-
JSON
isHashable
so you can use it as a dictionary key. -
JSON
isExpressibleBy*Literal
. That's why you can initialize w/variable:JSON
construct show above. -
JSON
isCustomStringConvertible
whose.description
is always a valid JSON. -
JSON
isCodable
. You can use this module instead ofJSONEncoder
. -
JSON
isSequence
. But when you iterate, be careful with the key.
let ja:JSON = [nil, true, 1, "one", [1], ["one":1]]
// wrong!
for v in ja {
//
}
// right!
for (i, v) in ja {
// i is NOT an Integer but KeyType.Index.
// To access its value, say i.index
}
let jo:JSON = [
"null":nil, "bool":false, "number":0, "string":"",
"array":[], "object":[:]
]
for (k, v) in jo {
// k is NOT an Integer but KeyType.Key.
// To access its value, say i.key
}
That is because swift demands to return same Element
type. If you feel this counterintuitive, you can simply use .array
or .object
:
for v in ja.array! {
// ...
}
for (k, v) in jo.object! {
// ...
}
Error handling
Once init
ed, JSON
never fails. That is, it never becomes nil
. Instead of being failable or throwing exceptions, JSON
has a special value .Error(.ErrorType)
which propagates across the method invocations. The following code examines the error should it happen.
if let e = json.error {
debugPrint(e.type)
if let nsError = e.nsError {
// do anything with nsError
}
}
Usage
build
$ git clone https://github.com/dankogai/swift-json.git
$ cd swift-json # the following assumes your $PWD is here
$ swift build
REPL
Simply
$ scripts/run-repl.sh
or
$ swift build && swift -I.build/debug -L.build/debug -lJSON
and in your repl,
1> import JSON
2> let json:JSON = ["swift":["safe","fast","expressive"]]
json: JSON.JSON = Object {
Object = 1 key/value pair {
[0] = {
key = "swift"
value = Array {
Array = 3 values {
[0] = String {
String = "safe"
}
[1] = String {
String = "fast"
}
[2] = String {
String = "expressive"
}
}
}
}
}
}
Xcode
Xcode project is deliberately excluded from the repository because it should be generated via swift package generate-xcodeproj
. For convenience, you can
$ scripts/prep-xcode
And the Workspace opens up for you with Playground on top. The playground is written as a manual.
iOS and Swift Playground
Unfortunately Swift Package Manager does not support iOS. To make matters worse Swift Playgrounds does not support modules. But don't worry. This module is so compact all you need is copy JSON.swift.
In case of Swift Playgrounds just copy it under Sources
folder. If you are too lazy just run:
$ scripts/ios-prep.sh
and iOS/JSON.playground
is all set. You do not have to import JSON
therein.
From Your SwiftPM-Managed Projects
Add the following to the dependencies
section:
.package(
url: "https://github.com/dankogai/swift-json.git", from: "4.0.0"
)
and the following to the .target
argument:
.target(
name: "YourSwiftyPackage",
dependencies: ["JSON"])
Now all you have to do is:
import JSON
in your code. Enjoy!
Prerequisite
Swift 4.1 or better, OS X or Linux to build.