Elevate is a JSON parsing framework that leverages Swift to make parsing simple, reliable and composable.
CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:
[sudo] gem install cocoapods
CocoaPods 1.0+ is required.
To integrate Elevate into your Xcode project using CocoaPods, specify it in your Podfile :
source 'https://github.com/CocoaPods/Specs.git' platform :ios, '9.0' use_frameworks! pod 'Elevate', '~> 1.0'
Carthage is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
You can install Carthage with Homebrew using the following command:
brew update brew install carthage
To integrate Elevate into your Xcode project using Carthage, specify it in yourCartfile:
github "Nike-Inc/Elevate" ~> 1.0
To build Elevate on iOS only, use the following Carthage command:
carthage update --platform iOS
Elevate aims to make JSON parsing and validation simple, yet robust. This is achieved through a set of protocols and classes that can be utilized to create Decodable
and Decoder
classes. By using Elevate's parsing infrastructure, you'll be able to easily parse JSON data into strongly typed model objects or simple dictionaries by specifying each property key path and its associated type. Elevate will validate that the keys exist (if they're not optional) and that they are of the correct type. Validation errors will be aggregated as the JSON data is parsed. If an error is encountered, a ParserError
will be thrown.
After you have made your model objects Decodable
or implemented a Decoder
for them, parsing with Elevate is as simple as:
let avatar: Avatar = try Parser.parseObject(data: data, forKeyPath: "response.avatar")
Pass an empty string into forKeyPath
if your object or array is at the root level.
In the previous example Avatar
implements the Decodable
protocol. By implementing the Decodable
protocol on an object, it can be used by Elevate to parse avatars from JSON data as a top-level object, a sub-object, or even an array of avatar objects.
public protocol Decodable { init(json: AnyObject) throws }
The json: AnyObject
will typically be a [String: AnyObject]
instance that was created from the NSJSONSerialization
APIs. Use the Elevate Parser.parseProperties
method to define the structure of the JSON data to be validated and perform the parsing.
struct Person: Decodable { let identifier: String let name: String let nickname: String? let birthDate: NSDate let isMember: Bool? let addresses: [Address] init(json: AnyObject) throws { let idKeyPath = "identifier" let nameKeyPath = "name" let nicknameKeyPath = "nickname" let birthDateKeyPath = "birthDate" let isMemberKeyPath = "isMember" let addressesKeyPath = "addresses" let dateDecoder = DateDecoder(dateFormatString: "yyyy-MM-dd") let properties = try Parser.parseProperties(json: json) { make in make.propertyForKeyPath(idKeyPath, type: .Int) make.propertyForKeyPath(nameKeyPath, type: .String) make.propertyForKeyPath(nicknameKeyPath, type: .String, optional: true) make.propertyForKeyPath(birthDateKeyPath, type: .String, decoder: dateDecoder) make.propertyForKeyPath(isMemberKeyPath, type: .Bool, optional: true) make.propertyForKeyPath(addressesKeyPath, type: .Array, decodedToType: Address.self) } self.identifier = properties <-! idKeyPath self.name = properties <-! nameKeyPath self.nickname = properties <-? nicknameKeyPath self.birthDate = properties <-! birthDateKeyPath self.isMember = properties <-? isMemberKeyPath self.addresses = properties <--! addressesKeyPath } }
Implementing the Decodable
protocol in this way allows you to create fully intialized structs that can contain non-optional constants from JSON data.
Some other things worth noting in this example:
Decodable
protocol conformance was implemented as an extension on the struct. This allows the struct to keep its automatic memberwise initializer. NSURL
, Array
, and Dictionary
types. See ParserPropertyType
definition for the full list. Decoder
for further manipulation. See the birthDate
property in the example above. The DateDecoder
is a standard Decoder
provided by Elevate to make date parsing hassle free. Decoder
or Decodable
type can be provided to a property of type .Array
to parse each item in the array to that type. This also works with the .Dictionary
type to parse a nested JSON object. Any
value from the properties
dictionary and cast it to the return type. Elevate contains four property extraction operators to make it easy to extract values out of the properties
dictionary and cast the Any
value to the appropriate type.
<-!
- Extracts the value from the properties
dictionary for the specified key. This operator should only be used on non-optional properties. <-?
- Extracts the optional value from the properties
dictionary for the specified key. This operator should only be used on optional properties. <--!
- Extracts the array from the properties
dictionary for the specified key as the specified array type. This operator should only be used on non-optional array properties. <--?
- Extracts the array from the properties
dictionary for the specified key as the specified optional array type. In most cases implementing a Decodable
model object is all that is needed to parse JSON using Elevate. There are some instances though where you will need more flexibility in the way that the JSON is parsed. This is where the Decoder
protocol comes in.
public protocol Decoder { func decodeObject(object: AnyObject) throws -> Any }
A Decoder
is generally implemented as a separate object that returns instances of the desired model object. This is useful when you have multiple JSON mappings for a single model object, or if you are aggregating data across multiple JSON payloads. For example, if there are two separate services that return JSON for Avatar
objects that have a slightly different property structure, a Decoder
could be created for each mapping to handle each one individually.
The input type and output types are intentionally vague to allow for flexibility. A Decoder
can return any type you want -- a strongly typed model object, a dictionary, etc. It can even dynamically return different types at runtime if needed.
class AvatarDecoder: Decoder { func decodeObject(object: AnyObject) throws -> Any { let urlKeyPath = "url" let widthKeyPath = "width" let heightKeyPath = "height" let properties = try Parser.parseProperties(json: json) { make in make.propertyForKeyPath(urlKeyPath, type: .URL) make.propertyForKeyPath(widthKeyPath, type: .Int) make.propertyForKeyPath(heightKeyPath, type: .Int) } return Avatar( URL: properties <-! urlKeyPath, width: properties <-! widthKeyPath, height: properties <-! heightKeyPath ) } }
class AlternateAvatarDecoder: Decoder { func decodeObject(object: AnyObject) throws -> Any { let locationKeyPath = "location" let wKeyPath = "w" let hKeyPath = "h" let properties = try Parser.parseProperties(json: json) { make in make.propertyForKeyPath(locationKeyPath, type: .URL) make.propertyForKeyPath(wKeyPath, type: .Int) make.propertyForKeyPath(hKeyPath, type: .Int) } return Avatar( URL: properties <-! locationKeyPath, width: properties <-! wKeyPath, height: properties <-! hKeyPath ) } }
Then to use the two different Decoder
objects with the Parser
:
let avatar1: Avatar = try Parser.parseObject( data: data1, forKeyPath: "response.avatar", withDecoder: AvatarDecoder() ) let avatar2: Avatar = try Parser.parseObject( data: data2, forKeyPath: "alternative.response.avatar", withDecoder: AlternateAvatarDecoder() )
Each Decoder
is designed to handle a different JSON structure for creating an Avatar
. Each uses the key paths specific to the JSON data it's dealing with, then maps those back to the properties on the Avatar
object. This is a very simple example to demonstration purposes. There are MANY more complex examples that could be handled in a similar manner via the Decoder
protocol.
A second use for the Decoder
protocol is to allow for the value of a property to be further manipulated. The most common example is a date string. Here is how the DateDecoder
implements the Decoder
protocol:
public func decodeObject(data: AnyObject) throws -> Any { if let string = data as? String { return try dateFromString(string, withFormatter:self.dateFormatter) } else { let description = "DateParser object to parse was not a String." throw ParserError.Validation(failureReason: description) } }
And here is how it's used to parse a JSON date string:
let dateDecoder = DateDecoder(dateFormatString: "yyyy-MM-dd 'at' HH:mm") let properties = try Parser.parseProperties(data: data) { make in make.propertyForKeyPath("dateString", type: .String, decoder: dateDecoder) }
You are free to create any decoders that you like and use them with your properties during parsing. Some other uses would be to create a StringToBoolDecoder
or StringToFloatDecoder
that parses a Bool
or Float
from a JSON string value. The DateDecoder
and StringToIntDecoder
are already included in Elevate for your convenience.