CocoaPods trunk is moving to be read-only. Read more on the blog, there are 14 months to go.
| TestsTested | ✓ |
| LangLanguage | SwiftSwift |
| License | MIT |
| ReleasedLast Release | Jun 2016 |
| SPMSupports SPM | ✓ |
Maintained by Anton Bronnikov.
There is a plenty of JSON libraries for Swift already. Why this one?
Foundation framework is not necessary.enum. Therefore, no more need to resort to obscure AnyObject.This library was inspired by SwiftyJSON, Gloss and TidyJSON.
The core of Iolcus framework is JSON type. In practice it’s just an enum:
enum JSON {
case Null
case Boolean(Bool)
case Integer(Int)
case Float(Double)
case String(String )
case Array([JSON])
case Object([String: JSON])
}JSON literalsIt’s possible to create an instance of JSON by just assigning some property with a literal:
import Iolcus
let jsonBoolean: JSON = true
let jsonInteger: JSON = 42
let jsonFloat: JSON = 36.6
let jsonString: JSON = "Lorem ipsum dolor sit amet"
let jsonArray: JSON = [false, 1, 42.0, "3"]
let jsonObject: JSON = [
"boolean" : false,
"integer" : -42,
"string" : "Lorem ipsum"
]JSON initializersSome other ways ways to instantiate JSON are:
// Direcly via enum's case
let jsonLUAE = JSON.Integer(42)
// By encoding JSON from an JSONEncodable-conforming instance
let jsonNotFalse = JSON(encoding: true)
// By copying another JSON's sub-element
let jsonSentence: JSON = ["Lorem", "ipsum", "dolor", "sit", "amet"]
let jsonFirstWord = try! JSON(json: jsonSentence, at: 0) // .String("Lorem")
let jsonPerson: JSON = ["name": "John Doe", "age": 42]
let jsonAge = try! JSON(json: jsonPerson, at: "age") // .Integer(42)
// By deserializing string representation of JSON
let serializedFibonacci = "[1, 1, 2, 3, 5, 8]"
let jsonFibonacci = try! JSON(jsonSerialization: serializedFibonacci)JSON Although, JSON is enum and as such can be used in standard Swift switch statement, or in if/case pattern, it might be eaiser to use the set of is... (isNull, isBoolean, isInteger, etc) computed properties to check which particular JSON kind is being handled at the moment.
For example:
let jsonStuff: JSON = [true, "Ook", 36.6, 999, "Eek"] // Prints:
//
for (_, element) in jsonStuff where element.isNumber { // Float 36.6
print(element.kind, element) // Integer 999
}JSON valuesEvery JSON value, except .Null, wraps either some elementary value (Bool, Int, Double or String) or a container ([JSON] or [String: JSON]). It is possible to access the value wrapped by JSON via special set of computed properties unwrapped... (unwrappedBoolean, unwrappedInteger, unwrappedFloat , etc). These properties return either a wrapped value (if the type matches) or nil (if there is no match).
For example:
let jsonEarth: JSON = [
"isFlat" : false,
"mass" : JSON(encoding: ["value": 5.972e24, "uom": "kg"]),
"radius" : JSON(encoding: ["value": 6371, "uom": "km"])
]
if let earthIsFlat = jsonEarth["isFlat"].unwrappedBoolean { // Prints:
if !earthIsFlat { //
print("Earth is not flat") // Earth is not flat
}
}
if let earthMass = jsonEarth["mass"]["value"].unwrappedString { // Prints nothing because
print("Earth's mass is \(earthMass)") // unwrappedString returns nil
} // for JSON.Float valuesIn addition to unwrapped... set of properties there is another set of coerced... properties. This set provides implicit conversion from actually wrapped elementary value into a target type. If such conversion is not possible then the result is, again, nil.
For example:
if let earthMass = jsonEarth["mass"]["value"].coercedString { // Prints:
print("Earth's mass is \(earthMass)") //
} // Earth's mass is 5.972e+24Elements of the container JSON kinds (that is, .Array and .Object) can be directly accessed via subscripts:
let jsonEarthIsFlat = jsonEarth["isFlat"] // .Boolean(false)Subscripts can be chained:
let jsonEarthMass = jsonEarth["mass"]["value"] // .Float(5.972e+24)Subscripts can be used as setters:
var jsonListOfOrders: JSON = [1000000, 1000001, 1000002] // Prints:
//
jsonListOfOrders[2] = 1000005 // [
jsonListOfOrders[1] = 1000003 // 1000007,
jsonListOfOrders[0] = 1000007 // 1000003,
// 1000005
print(jsonListOfOrders) // ]In addition to Int and String-indexed subscripts, there is a special case of Void-indexed subscript []:
var jsonDynamicallyGrownArray: JSON = [] // Prints:
//
jsonDynamicallyGrownArray[].append("start" ) // [
jsonDynamicallyGrownArray[].append(".") // "start",
jsonDynamicallyGrownArray[].append("..") // ".",
jsonDynamicallyGrownArray[].append("...end") // "..",
// "...end"
print(jsonDynamicallyGrownArray) // ]Main purpose of [] subcript is to provide a way to create/grow/shrink JSON.Array:
.Array, it unwraps and returns underlying array of JSON sub-elements.JSON kind, it will create and return single-element array out of it.[JSON] array and create JSON.Array out of it.For example, the following snippet will produce exactly same output as the one from above:
var jsonImplicitlyConvertedArray: JSON = “start” // Prints: // jsonImplicitlyConvertedArray[].append(“.” ) // [ jsonImplicitlyConvertedArray[].append(“..”) // “start”, jsonImplicitlyConvertedArray[].append(“…end”) // “.”, // “..”, print(jsonImplicitlyConvertedArray) // “…end” // ]
Encoding of a custom type into JSON is enabled by implementing JSONEncodable protocol.
For example, if we have a structure called Book:
struct Book {
let title : String
let pagesCount : Int
let isPaperback : Bool
let authors : [String]
let notes : [String: String]
}… then it can implement JSONEncodable as follows:
extension Book: JSONEncodable {
func jsonEncoded() -> JSON {
return [
"title" : title,
"pages" : pagesCount,
"isPaperback" : isPaperback,
"authors" : JSON(encoding: authors),
"notes" : JSON(encoding: notes)
]
}
}Note: Array
authorsand dictionarynoteshave to be explicitly encoded intoJSONviaJSON(encoding: _)initializer. This is because there is no support (yet) for conditional protocol conformance by generics. It might appear in Swift 3, though.
Obvioulsy, with JSONEncodable implemented, we can encode Book instances into JSON by calling the method we just implemented:
let book = Book( // Prints:
title : "Dune", //
pagesCount : 896, // {
isPaperback : true, // "title": "Dune",
authors : ["Frank Herbert"], // "isPaperback": true,
notes : [ // "authors": [
"2015-05-23": "Damaged. Handed over to repair.", // "Frank Herbert"
"2015-06-10": "Repaired. Condition is good." // ],
] // "notes": {
) // "2015-05-23": "Damaged. Handed over to repair.",
// "2015-06-10": "Repaired. Condition is good."
var jsonBook = book.jsonEncoded() // },
// "pages": 896
print(jsonBook) // }… or alternatively we can use the initializer:
let jsonAnotherBook = JSON(encoding: book)As soon as Book is JSONEncodable we can also encode arrays and dictionaries that contain Book:
struct Library: JSONEncodable {
let books: [Book]
let favorites: [String: Book]
let series: [String: [Book]]
func jsonEncoded() -> JSON {
return [
"books" : JSON(encoding: books),
"favorites" : JSON(encoding: favorites),
"series" : JSON(encoding: series)
]
}
}Decoding of JSON is also made possible via protocol implementation. In this case it’s JSONDecodable protocol.
Keeping up with our Book as an example:
extension Book: JSONDecodable {
init(json: JSON) throws {
title = try json.decode(at: "title")
pagesCount = try json.decode(at: "pages")
isPaperback = try json.decode(at: "isPaperback")
authors = try json.decode(at: "authors")
notes = try json.decode(at: "notes")
}
}Same thing for Library:
extension Library: JSONDecodable {
init(json: JSON) throws {
books = try json.decode(at: "books")
favorites = try json.decode(at: "favorites")
series = try json.decode(at: "series")
}
}As soon as JSONDecodable is implemented, decoding of a new instance from JSON takes just a call to the initializer:
let anotherBook = try! Book(json: jsonBook)Method jsonSerialized() should be used both to serialize JSON values:
let serializedBook = jsonBook.jsonSerialized()… as well as instances that conform to JSONEncodable protocol:
let anotherSerializedBook = book.jsonSerialized()Special form of initializer JSON(jsonSerialization: _) can be used to deserialize JSON values:
Note: Theoretically, a string that is used as an input for deserialization can be invalid. That is why
JSON(jsonSerialization: _)call can throw an error ofJSON.Error.Deserializingtype.
do {
let jsonDeserialized = try JSON(jsonSerialization: serializedBook)
}
catch let error as JSON.Error.Deserializing {
// Deserializing JSON failed. Do something about it.
}Similar form of initializer in available for types conforming to JSONDecodable protocol:
Note: Deserialization of
Bookcan throw a more error types. On top ofJSON.Error.Deserializingit can also throwJSON.Error.DecodingandJSON.Error.Subscripting:
do {
let yetAnotherBook = try Book(jsonSerialization: serializedBook)
}
catch let error as JSON.Error.Deserializing {
// Something went wrong during deserialization of JSON from input string
}
catch let error as JSON.Error.Decoding {
// Something went wrong while decoding of JSON into target type
}
catch let error as JSON.Error.Subscripting {
// Something went wrong while reading values from container JSON elements
}JSON Container JSON kinds (.Object and .Array) can be iterated over. For example, with forEach() method:
let jsonTodoList: JSON = ["Groceries", "Pick up kids", "Dinner"] // Prints:
//
jsonTodoList.forEach { // [0] "Groceries"
print($0, $1) // [1] "Pick up kids"
} // [2] "Dinner"Note: Non-container
JSONkinds can be iterated over as well, only there will be just one iteration made.Note:
JSONIndexisenumwith three cases:.Thisused for non-containerJSON,.Index(Int)used for.Arrayand.Key(String)used for.Object.
Naturally, map(), filter() and all other methods applicable to sequences can be used with JSON too:
let pagerCountIndex = jsonBook.filter { $1 == 896 } // Prints:
.first?.index //
print(pagerCountIndex) // Optional(["pages"])Iterating over JSON in a standard for-loop will only go over the surface without going deeper down into nested JSON sub-elements. For this reason JSON offers special method flatten() that constructs an array of (path: JSONPath, elemetaryValue: JSON) tuples. Such array will only contain basic JSON values (.Null, .Boolean, .Integer, .Float and .String), and each such value will be accompanied with a JSONPath that holds the indexes that have to be traversed from the root of self and all the way down to the particular basic element.
For example:
let dune = Book( // Prints:
title : "Dune", //
pagesCount : 896, // {
isPaperback : true, // "title": "Dune",
authors : ["Frank Herbert"], // "isPaperback": true,
notes : [ // "authors": [
"2015-05-23": "Damaged. Handed over to repair.", // "Frank Herbert"
"2015-06-10": "Repaired. Condition is good." // ],
] // "notes": {
) // "2015-05-23": "Damaged. Handed over to repair.",
// "2015-06-10": "Repaired. Condition is good."
let jsonDune = book.jsonEncoded() // },
// "pages": 896
print(jsonDune) // }
print() //
// jsonDune["title"] == "Dune"
jsonDune.flatten().forEach { // jsonDune["isPaperback"] == true
(path: JSONPath, json: JSON) in // jsonDune["authors"][0] == "Frank Herbert"
// jsonDune["notes"]["2015-05-23"] == "Damaged. Handed over to repair."
print("jsonDune\(path) == \(json)") // jsonDune["notes"]["2015-06-10"] == "Repaired. Condition is good."
} // jsonDune["pages"] == 896
// ["notes"]["2015-05-23"]Note:
JSONPathis essentially an array ofJSONIndexvalues. It can be constructed from the sequence ofJSONIndexvalues or from a literal. It can be used in the subsript accessor as well.