Serpent (previously known as Serializable) is a framework made for creating model objects or structs that can be easily serialized and deserialized from/to JSON. It's easily expandable and handles all common data types used when consuming a REST API, as well as recursive parsing of custom objects. Designed for use with Alamofire.
It's designed to be used together with our helper app, the Model Boiler, making model creation a breeze.
Serpent is implemented using protocol extensions and static typing.
📑 Table of Contents
🐍 Why Serpent?
There are plenty of other Encoding and Decoding frameworks available. Why should you use Serpent?
- Performance. Serpent is fast, up to 4x faster than similar frameworks.
- Features. Serpent can parse anything you throw at it. Nested objects, Enums, URLs, UIColor, you name it!
- Model Boiler. Every framework of this kind requires tedious boilerplate code that takes forever to write. Model Boiler generates it for you instantly.
- Alamofire Integration. Using the included Alamofire extensions makes implementing an API call returning parsed model data as simple as doing a one-liner!
- Expandability. Parsing into other datatypes can easily be added.
- Persisting. Combined with our caching framework Cashier, Serpent objects can be very easily persisted to disk.
- Serpent Xcode File Template makes it easier to create the model files in Xcode.
📝 Requirements
- iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+
- Swift 3.0+
(Swift 2.2 & Swift 2.3 supported in older versions)
📦 Installation
Carthage
github "nodes-ios/Serpent" ~> 1.0
Last versions compatible with lower Swift versions:
Swift 2.3
github "nodes-ios/Serpent" == 0.13.2
Swift 2.2
github "nodes-ios/Serpent" == 0.11.2
NOTE: Serpent was previously known as Serializable.
CocoaPods
Choose one of the following, add it to your Podfile
and run pod install
:
pod 'Serpent', '~> 1.0' # Just core
pod 'Serpent/Extensions', '~> 1.0' # Includes core and all extensions
pod 'Serpent/AlamofireExtension', '~> 1.0' # Includes core and Alamofire extension
pod 'Serpent/CashierExtension', '~> 1.0' # Includes core and Cashier extension
NOTE: CocoaPods only supports Serpent using Swift version 3.0 and higher.
Swift Package Manager
To use Serpent as a Swift Package Manager package just add the following to your Package.swift
file.
import PackageDescription
let package = Package(
name: "YourPackage",
dependencies: [
.Package(url: "https://github.com/nodes-ios/Serpent.git", majorVersion: 1)
]
)
🔧 Setup
We highly recommend you use our Model Boiler to assist with generating the code needed to conform to Serpent. Instructions for installation and usage can be found at the Model Boiler GitHub repository.
💻 Usage
Getting started
Serpent supports all primitive types, enum
, URL
, Date
, UIColor
, other Serpent
model, and Array
of all of the aforementioned types. Your variable declarations can have a default value or be optional.
Primitive types do not need to have an explicit type, if Swift is able to infer it normally. var name: String = ""
works just as well as var name = ""
. Optionals will of course need an explicit type.
NOTE: Enums you create must conform to
RawRepresentable
, meaning they must have an explicit type. Otherwise, the parser won't know what to do with the incoming data it receives.
Create your model struct or class:
struct Foo {
var id = 0
var name = ""
var address: String?
}
NOTE: Classes must be marked
final
.
Encodable
and Decodable
:
Add the required methods for extension Foo: Serializable {
init(dictionary: NSDictionary?) {
id <== (self, dictionary, "id")
name <== (self, dictionary, "name")
address <== (self, dictionary, "address")
}
func encodableRepresentation() -> NSCoding {
let dict = NSMutableDictionary()
(dict, "id") <== id
(dict, "name") <== name
(dict, "address") <== address
return dict
}
}
NOTE: You can add conformance to
Serializable
which is a type alias for bothEncodable
andDecodable
.
And thats it! If you're using the Model Boiler, this extension will be generated for you, so that you don't need to type it all out for every model you have.
Using Serpent models
New instances of your model can be created with a dictionary, e.g. from parsed JSON.
let dictionary = try JSONSerialization.jsonObject(with: someData, options: .allowFragments) as? NSDictionary
let newModel = Foo(dictionary: dictionary)
You can generate a dictionary version of your model by calling encodableRepresentation()
:
let encodedDictionary = newModel.encodableRepresentation()
More complex examples
In this example, we have two models, Student and School.
struct Student {
enum Gender: String {
case male = "male"
case female = "female"
case unspecified = "unspecified"
}
var name = ""
var age: Int = 0
var gender: Gender?
}
struct School {
enum Sport: Int {
case football
case basketball
case tennis
case swimming
}
var name = ""
var location = ""
var website: URL?
var students: [Student] = []
var sports: [Sport]?
}
You can get as complicated as you like, and the syntax will always remain the same. The extensions will be:
extension Student: Serializable {
init(dictionary: NSDictionary?) {
name <== (self, dictionary, "name")
age <== (self, dictionary, "age")
gender <== (self, dictionary, "gender")
}
func encodableRepresentation() -> NSCoding {
let dict = NSMutableDictionary()
(dict, "name") <== name
(dict, "age") <== age
(dict, "gender") <== gender
return dict
}
}
extension School: Serializable {
init(dictionary: NSDictionary?) {
name <== (self, dictionary, "name")
location <== (self, dictionary, "location")
website <== (self, dictionary, "website")
students <== (self, dictionary, "students")
sports <== (self, dictionary, "sports")
}
func encodableRepresentation() -> NSCoding {
let dict = NSMutableDictionary()
(dict, "name") <== name
(dict, "location") <== location
(dict, "website") <== website
(dict, "students") <== students
(dict, "sports") <== sports
return dict
}
}
Again, the Model Boiler generates all of this code for you in less than a second!
Using with Alamofire
Serpent comes integrated with Alamofire out of the box, through an extension on Alamofire's Request
construct, that adds the function responseSerializable(completion:unwrapper)
The extension uses Alamofire's familiar Response
type to hold the returned data, and uses its generic associated type to automatically parse the data.
Consider an endpoint returning a single school
structure matching the struct from the example above. To implement the call, simply add a function to your shared connection manager or where ever you like to put it:
func requestSchool(completion: @escaping (DataResponse<School>) -> Void) {
request("http://somewhere.com/school/1", method: .get).responseSerializable(completion)
}
In the consuming method you use it like this:
requestSchool() { (response) in
switch response.result {
case .success(let school):
//Use your new school object!
case .failure(let error):
//Handle the error object, or check your Response for more detail
}
}
For an array of objects, use the same technique:
static func requestStudents(completion: @escaping (DataResponse<[Student]>) -> Void) {
request("http://somewhere.com/school/1/students", method: .get).responseSerializable(completion)
}
Some APIs wrap their data in containers. Use the unwrapper
closure for that. Let's say your /students
endpoint returns the data wrapped in a students
object:
{
"students" : [
{
"..." : "..."
},
{
"..." : "..."
}
]
}
The unwrapper
closure has 2 input arguments: The sourceDictionary
(the JSON Response Dictionary) and the expectedType
(the type of the target Serpent). Return the object that will serve as the input for the Serializable initializer.
static func requestStudents(completion: (DataResponse<[Student]>) -> Void) {
request("http://somewhere.com/school/1/students", method: .get).responseSerializable(completion, unwrapper: { $0.0["students"] })
}
If you need to unwrap the response data in every call, you can install a default unwrapper using
Parser.defaultWrapper = { sourceDictionary, expectedType in
// You custom unwrapper here...
return sourceDictionary
}
The expectedType
can be used to dynamically determine the key based on the type name using reflection. This is especially useful when handling paginated data.
See here for an example on how we use this in our projects at Nodes.
NOTE: responseSerializable
Internally calls validate().responseJSON()
on the request, so you don't have to do that.
Date parsing
Serpent can create Date
objects from the date strings in the JSON. By default, Serpent can parse the date strings from the following formats: yyyy-MM-dd'T'HH:mm:ssZZZZZ
, yyyy-MM-dd'T'HH:mm:ss
, yyyy-MM-dd
. If you need to parse other date formats, you can do it by adding this line to your code (for example, in AppDelegate
's didFinishLaunchingWithOptions:
:
Date.customDateFormats = ["yyyyMMddHHmm", "yyyyMMdd"] // add the custom date formats you need here
The custom date formats won't replace the default ones, they will be still supported.
👥 Credits
Made with
📄 License
Serpent is available under the MIT license. See the LICENSE file for more info.