A protocol to serialize Swift structs and classes for encoding and decoding.
Swift Objective-C
Latest commit 9dcb8d2 Mar 2, 2017 @chriscombs chriscombs committed on GitHub Merge pull request #130 from nodes-ios/develop
Update README on master

README.md

Serpent

Travis Codecov codebeat badge Carthage Compatible CocoaPods
Plaforms GitHub license

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 ModelBoiler 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!
  • ModelBoiler Model Boiler. Every framework of this kind requires tedious boilerplate code that takes forever to write. ModelBoiler 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 ModelBoiler 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.

Add the required methods for Encodable and Decodable:

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 both Encodable and Decodable.

And thats it! If you're using the ModelBoiler 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 ModelBoiler 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 ❀️ at Nodes.

πŸ“„ License

Serpent is available under the MIT license. See the LICENSE file for more info.