Himotoki Tutorial
Himotoki is a simple yet powerful library for decoding JSON. From the project page:
Himotoki (紐解き) is a type-safe JSON decoding library purely written in Swift. This library is highly inspired by popular JSON parsing libraries in Swift: Argo and ObjectMapper.
This article provides a brief tutorial and Xcode Playground to help learn how to use it. You can clone the Playground from GitHub here.
Basic Decoding
As you can see from the Himotoki README, creating a model and specifying how to decode it from JSON is fairly straightforward:
struct Group: Decodable {
let name: String
let floor: Int
let locationName: String
let optional: [String]?
// MARK: Decodable
static func decode(e: Extractor) throws -> Group {
return try Group(
name: e <| "name",
floor: e <| "floor",
locationName: e <| [ "location", "name" ], // Parse nested objects
optional: e <||? "optional" // Parse optional arrays of values
)
}
}
Himotoki can decode any generic type T
conforming to its Decodable
protocol using the operators below. We’ll see how to implement Decodable in our own code later.
Operator | Decode element as | Remarks |
---|---|---|
<| |
T |
A value |
<|? |
T? |
An optional value |
<|| |
[T] |
An array of values |
<||? |
[T]? |
An optional array of values |
<|-| |
[String: T] |
A dictionary of values |
<|-|? |
[String: T]? |
An optional dictionary of values |
Advanced Decoding
Let’s assume we have the following JSON representing a series of musical bands:
{
"objects": [
{
"name": "Beatles",
"homepage": "http://www.thebeatles.com/",
"active": "inactive",
"members": [
{
"name": "George Harrison",
"birth_date": "1943-02-25"
},
{
"name": "John Lennon",
"birth_date": "1940-10-09"
},
{
"name": "Paul McCartney",
"birth_date": "1942-06-18"
},
{
"name": "Ringo Starr",
"birth_date": "1940-07-07"
}
]
},
{
"name": "The Rolling Stones",
"active": "active",
"members": [
{
"name": "Charlie Watts",
"birth_date": "1941-06-02"
},
{
"name": "Keith Richards",
"birth_date": "1943-12-18"
},
{
"name": "Mick Jagger",
"birth_date": "1943-07-26"
},
{
"name": "Ronnie Wood",
"birth_date": "1947-06-01"
}
]
}
],
"last_updated": "2016-05-17T22:43:04.000000+00:00"
}
This JSON is straightforward, yet interesting enough to give our model several requirements. There are a couple of different date formats (last_updated
and birth_date
), and a field which can be represented as an enumerated type (active
). The optional homepage
property should really be represented as an NSURL
. Finally, this structure represents a generic collection of objects
, so it would be nice to re-use that collection for other types.
Models
We define the following struct
types to model our data.
struct BandResponse {
let objects: [Band]
let lastUpdated: NSDate?
}
struct Band {
enum Status: String {
case Active = "active"
case Hiatus = "hiatus"
case Disbanded = "inactive"
}
let name: String
let members: [BandMember]
let homepageURL: NSURL?
let status: Status
}
struct BandMember {
let name: String
let birthDate: NSDate
}
Decodable
Through the power of generics, Himotoki can automatically decode extracted values from JSON to their correct types. To add support for NSURL
, we define an Extension:
extension NSURL: Decodable {
public static func decode(e: Extractor) throws -> Self {
let rawValue = try String.decode(e)
guard let result = self.init(string: rawValue) else {
throw DecodeError.Custom("Error parsing URL from string")
}
return result
}
}
We also need to implement Decodable for our own types, Band
, BandMember
and BandResponse
:
extension BandResponse: Decodable {
static func decode(e: Extractor) throws -> BandResponse {
return try BandResponse(objects: e <|| "objects",
lastUpdated: DateTimeTransformer.apply(e <|? "last_updated")
)
}
}
extension BandMember: Decodable {
static func decode(e: Extractor) throws -> BandMember {
return try BandMember(name: e <| "name",
birthDate: DateTransformer.apply(e <| "birth_date")
)
}
}
extension Band: Decodable {
static func decode(e: Extractor) throws -> Band {
return try Band(name: e <| "name",
members: e <|| "members",
homepageURL: e <|? "homepage",
status: e <| "active"
)
}
}
extension Band.Status: Decodable {}
Note that Himotoki provides a protocol extension for RawRepresentable
types, so conforming to Decodable
is enough as enums with a RawValue
conform to this protocol.
Finally, we implement two transformers for decoding the two date formats:
public let DateTransformer = Transformer<String, NSDate> { dateString throws -> NSDate in
let dateFormatter = NSDateFormatter()
dateFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd"
dateFormatter.timeZone = NSTimeZone(abbreviation: "UTC")
if let date = dateFormatter.dateFromString(dateString) {
return date
}
throw customError("Invalid date string: \(dateString)")
}
public let DateTimeTransformer = Transformer<String, NSDate> { dateString throws -> NSDate in
let dateFormatter = NSDateFormatter()
dateFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ'"
dateFormatter.timeZone = NSTimeZone(abbreviation: "UTC")
if let date = dateFormatter.dateFromString(dateString) {
return date
}
throw customError("Invalid datetime string: \(dateString)")
}
These only vary in the dateFormat
provided to the NSDateFormatter
, so we could probably factor this out.
If all date representations in JSON have the same format, we can define an Extension
on NSDate
to implement Decodable
, just as we did with NSURL
.
Given the above, we can now decode some JSON:
do {
let response = try BandResponse.decodeValue(bandJSON)
print(response?.objects.first?.name) // Beatles
} catch {
print(error)
}
If the JSON is valid, you’ll have a BandResponse
representation, and if not, error
will contain information on the first failure.
A generic Response wrapper
We can improve upon our BandResponse struct by using generics. We define a struct
Response
which is generic over Decodable
:
struct Response<T: Decodable> {
let objects: [T]
let lastUpdated: NSDate?
}
and implement Decodable
:
extension Response: Decodable {
static func decode<T: Decodable>(e: Extractor) throws -> Response<T> {
return try Response<T>(objects: e <|| "objects",
lastUpdated: DateTimeTransformer.apply(e <|? "last_updated")
)
}
}
Now we can decode the JSON to our generic collection:
do {
let response = try Response<Band>.decodeValue(bandJSON)
print(response?.objects.first?.name) // Beatles
} catch {
print(error)
}
And we’re done!
Whether you’re building an app or an API wrapper, I recommend trying Himotoki for your next project. You can even clone my Playground from GitHub and have a play with your own models and JSON.