Elevate is a JSON parsing framework that leverages Swift to make parsing simple, reliable and composable.
Elevate should no longer be used for new feature development.
We recommend using the Codable protocol provided by Apple in the Foundation framework in its place.
We will continue to support and update Elevate for the foreseeable future.
Features
Validation of full JSON payload
Parse complex JSON into strongly typed objects
Support for optional and required values
Convenient and flexible protocols to define object parsing
Large object graphs can be parsed into their component objects
Want to contribute? Fork the repo and submit a pull request.
Installation
CocoaPods
CocoaPods is a dependency manager for Cocoa projects.
You can install it with the following command:
[sudo] gem install cocoapods
CocoaPods 1.3+ 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, '11.0'
use_frameworks!
pod 'Elevate', '~> 3.0'
Carthage
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 your Cartfile:
github "Nike-Inc/Elevate" ~> 3.0
To build Elevate on iOS only, use the following Carthage command:
carthage update --platform iOS
Usage
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.
Elevate also supports encoding model objects back into JSON objects through the light-weight Encodable protocol.
Convenience extensions have been added to collection types to make it easy to encode nested objects in a single pass.
Parsing JSON with Elevate
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 Elevate.decodeObject(from: data, atKeyPath: "response.avatar")
Pass an empty string into atKeyPath if your object or array is at the root level.
Creating Decodables
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: Any) throws
}
The json: Any will typically be a [String: Any] instance that was created from the JSONSerialization APIs.
Use the Elevate Parser.parseEntity method to define the structure of the JSON data to be validated and perform the parsing.
struct Person {
let identifier: String
let name: String
let nickname: String?
let birthDate: Date
let isMember: Bool?
let addresses: [Address]
}
extension Person: Elevate.Decodable {
fileprivate struct KeyPath {
static let id = "identifier"
static let name = "name"
static let nickname = "nickname"
static let birthDate = "birthDate"
static let isMember = "isMember"
static let addresses = "addresses"
}
init(json: Any) throws {
let dateDecoder = DateDecoder(dateFormatString: "yyyy-MM-dd")
let entity = try Parser.parseEntity(json: json) { schema in
schema.addProperty(keyPath: KeyPath.id, type: .int)
schema.addProperty(keyPath: KeyPath.name, type: .string)
schema.addProperty(keyPath: KeyPath.nickname, type: .string, optional: true)
schema.addProperty(keyPath: KeyPath.birthDate, type: .string, decoder: dateDecoder)
schema.addProperty(keyPath: KeyPath.isMember, type: .bool, optional: true)
schema.addProperty(keyPath: KeyPath.addresses, type: .array, decodableType: Address.self)
}
self.identifier = entity <-! KeyPath.id
self.name = entity <-! KeyPath.name
self.nickname = entity <-? KeyPath.nickname
self.birthDate = entity <-! KeyPath.birthDate
self.isMember = entity <-? KeyPath.isMember
self.addresses = entity <--! KeyPath.addresses
}
}
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:
The Decodable protocol conformance was implemented as an extension on the struct.
This allows the struct to keep its automatic memberwise initializer.
Standard primitive types are supported as well as URL, Array, and Dictionary types.
See ParserPropertyProtocol definition for the full list.
Elevate facilitates passing a parsed property into a 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.
A 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.
The parser guarantees that properties will be of the specified type.
Therefore, it is safe to use the custom operators to automatically extract the Any value from the entity dictionary and cast it to the return type.
Property Extraction Operators
Elevate contains four property extraction operators to make it easy to extract values out of the entity dictionary and cast the Any value to the appropriate type.
<-! - Extracts the value from the entity dictionary for the specified key.
This operator should only be used on non-optional properties.
<-? - Extracts the optional value from the entity dictionary for the specified key.
This operator should only be used on optional properties.
<--! - Extracts the array from the entity 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 entity dictionary for the specified key as the specified optional array type.
Creating Encodables
Extending a model object to conform to the Encodable protocol is less involved than making it Decodable.
Since your object is already strongly typed, it only needs to be converted into a JSON friendly Any object.
Building on the previous Person type, let’s make it conform to the Encodable protocol.
extension Person: Elevate.Encodable {
var json: Any {
var json: [String: Any] = [
KeyPath.id: identifier,
KeyPath.name: name,
KeyPath.birthDate: birthDate,
KeyPath.addresses: addresses.json
]
if let nickname = nickname { json[KeyPath.nickname] = nickname }
if let isMember = isMember { json[KeyPath.isMember] = isMember }
return json
}
}
As you can see in the example, converting the Person into a JSON dictionary is straightforward.
It’s also easy to convert the array of Address objects into JSON by calling the json property on the array.
This works because Address also conforms to Encodable.
The collection type extensions on Array, Set and Dictionary make it easy to convert a complex objects with multiple layers of Encodable objects into a JSON objects.
Advanced Usage
Decoders
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 decode(_ object: Any) 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 them 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.
Using Multiple Decoders
class AvatarDecoder: Elevate.Decoder {
func decode(_ object: Any) throws -> Any {
let urlKeyPath = "url"
let widthKeyPath = "width"
let heightKeyPath = "height"
let entity = try Parser.parseEntity(json: object) { schema in
schema.addProperty(keyPath: urlKeyPath, type: .url)
schema.addProperty(keyPath: widthKeyPath, type: .int)
schema.addProperty(keyPath: heightKeyPath, type: .int)
}
return Avatar(
URL: entity <-! urlKeyPath,
width: entity <-! widthKeyPath,
height: entity <-! heightKeyPath
)
}
}
class AlternateAvatarDecoder: Elevate.Decoder {
func decode(_ object: Any) throws -> Any {
let locationKeyPath = "location"
let wKeyPath = "w"
let hKeyPath = "h"
let entity = try Parser.parseEntity(json: object) { schema in
schema.addProperty(keyPath: locationKeyPath, type: .url)
schema.addProperty(keyPath: wKeyPath, type: .int)
schema.addProperty(keyPath: hKeyPath, type: .int)
}
return Avatar(
URL: entity <-! locationKeyPath,
width: entity <-! wKeyPath,
height: entity <-! hKeyPath
)
}
}
Then to use the two different Decoder objects with the Parser:
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.
Decoders as Property Value Transformers
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 decode(_ object: Any) throws -> Any {
if let string = object 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 entity = try Parser.parseEntity(data: data) { schema in
schema.addProperty(keyPath: "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.
Elevate
Elevate is a JSON parsing framework that leverages Swift to make parsing simple, reliable and composable.
Features
Requirements
Communication
Installation
CocoaPods
CocoaPods is a dependency manager for Cocoa projects. You can install it with the following command:
To integrate Elevate into your Xcode project using CocoaPods, specify it in your Podfile:
Carthage
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:
To integrate Elevate into your Xcode project using Carthage, specify it in your Cartfile:
To build Elevate on iOS only, use the following Carthage command:
Usage
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
andDecoder
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, aParserError
will be thrown.Elevate also supports encoding model objects back into JSON objects through the light-weight
Encodable
protocol. Convenience extensions have been added to collection types to make it easy to encode nested objects in a single pass.Parsing JSON with Elevate
After you have made your model objects
Decodable
or implemented aDecoder
for them, parsing with Elevate is as simple as:Creating Decodables
In the previous example
Avatar
implements theDecodable
protocol. By implementing theDecodable
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.The
json: Any
will typically be a[String: Any]
instance that was created from theJSONSerialization
APIs. Use the ElevateParser.parseEntity
method to define the structure of the JSON data to be validated and perform the parsing.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.URL
,Array
, andDictionary
types. SeeParserPropertyProtocol
definition for the full list.Decoder
for further manipulation. See thebirthDate
property in the example above. TheDateDecoder
is a standardDecoder
provided by Elevate to make date parsing hassle free.Decoder
orDecodable
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 theentity
dictionary and cast it to the return type.Property Extraction Operators
Elevate contains four property extraction operators to make it easy to extract values out of the
entity
dictionary and cast theAny
value to the appropriate type.<-!
- Extracts the value from theentity
dictionary for the specified key. This operator should only be used on non-optional properties.<-?
- Extracts the optional value from theentity
dictionary for the specified key. This operator should only be used on optional properties.<--!
- Extracts the array from theentity
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 theentity
dictionary for the specified key as the specified optional array type.Creating Encodables
Extending a model object to conform to the
Encodable
protocol is less involved than making itDecodable
. Since your object is already strongly typed, it only needs to be converted into a JSON friendlyAny
object. Building on the previousPerson
type, let’s make it conform to theEncodable
protocol.As you can see in the example, converting the
Person
into a JSON dictionary is straightforward. It’s also easy to convert the array ofAddress
objects into JSON by calling thejson
property on the array. This works becauseAddress
also conforms toEncodable
. The collection type extensions onArray
,Set
andDictionary
make it easy to convert a complex objects with multiple layers ofEncodable
objects into a JSON objects.Advanced Usage
Decoders
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 theDecoder
protocol comes in.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 forAvatar
objects that have a slightly different property structure, aDecoder
could be created for each mapping to handle them individually.Using Multiple Decoders
Then to use the two different
Decoder
objects with theParser
:Each
Decoder
is designed to handle a different JSON structure for creating anAvatar
. Each uses the key paths specific to the JSON data it’s dealing with, then maps those back to the properties on theAvatar
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 theDecoder
protocol.Decoders as Property Value Transformers
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 theDateDecoder
implements theDecoder
protocol:And here is how it’s used to parse a JSON date string:
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
orStringToFloatDecoder
that parses aBool
orFloat
from a JSON string value. TheDateDecoder
andStringToIntDecoder
are already included in Elevate for your convenience.Creators