A protocol to serialize Swift structs and classes for encoding and decoding.

Overview

Serpent

CircleCI 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.

Comments
  • New OS targets

    New OS targets

    Added support for tvOS, watchOS, and OSX.

    Part of this required separating all of the HexInitializable code into a separate file, so that it could be excluded from the OSX target

    opened by chriscombs 8
  • Updating Travis

    Updating Travis

    I want to update Travis to use Xcode 8.3 and to also build the example project on the lowest supported target.

    Since I need to actually open a PR to see if this works, this PR is work in progress until further notice, so don't merge it yet

    opened by mariusc 5
  • Readme update

    Readme update

    • [x] mention that it was previously known as Serializable
    • [x] update the requirements to mention macOS/tvOS/watchOS
    • [x] update the code examples in the README to be Swift 3
    • [x] maybe mention older versions support in requirements (like in Installation - Carthage)
    enhancement 
    opened by mariusc 5
  • Allow the developer to fail the request in implementation even if it returned 200

    Allow the developer to fail the request in implementation even if it returned 200

    There are some bad APIs that return 200, but in the response JSON they return a status code that indicates an error.

    image

    It would be nice to have a way of manually checking for that and go to failure if necessary. In the example above, if ReturnCode is different than 0, the Failure case would be called and not the Success one (in the current implementation, it goes to Success).

    Maybe returning nil in the Unwrapper should make it go to Failure instead of Success with the empty model.

    enhancement 
    opened by mariusc 5
  • One final master/dev tag and make swift-3.0 new master

    One final master/dev tag and make swift-3.0 new master

    I'm not sure if we're ready to break everyone's cartfiles yet, but we should coordinate across the other frameworks as well.

    The master/dev branches do not have any features that swift-3.0 doesn't; the most recent commits to those are just updating to swift 2.3 and fixing travis. swift-3.0 has newer fixes and updates.

    enhancement 
    opened by chriscombs 5
  • New comment style, indentation issues

    New comment style, indentation issues

    Serpent is still using the old comment style, which is no longer recommended by Apple, so we should switch to the new standard (/// on each line).

    There's also some indentation issues in the project, which should be fixed. (maybe we could implement SwiftLint?)

    enhancement 
    opened by dominik-hadl 3
  • NSURLs are not parsed properly from a dictionary if it has a source type of NSURL

    NSURLs are not parsed properly from a dictionary if it has a source type of NSURL

    Serializable saves it as a NSURL when encoding, resulting in the parser not parsing it correctly when deserialising from disk (Looks like it enters the mapped overload for StringInitializable and fails when it gets out a NSURL instead of a String)

    opened by kasperwelner 3
  • Add support for mapping nested values

    Add support for mapping nested values

    This might be useful in cases where we don't need to parse a whole nested object and just care about one value.

    For example:

    JSON

    {
        "name": "Your name",
        "times": {
            "createdAt": "some time",
            "updatedAt": "some time",
            "deletedAt": "some time"
        }
    }
    

    Model

    struct User {
        var name = ""
        var createdAt = Date() // <-times/createdAt
    }
    
    opened by dominik-hadl 2
  • Remove static defaultUnwrapper on Parser

    Remove static defaultUnwrapper on Parser

    This will force people to specify an unwrapper manually and prevent issues with a global "magic" unwrapper.

    To imagine an example where this is problematic take Module A, where responseSerializable(completion) is called with no unwrapper and expects to receive whatever was passed in. Then Module B changes the Parser.defaultUnwrapper to something needed for itself. Module A does same action with the responseSerializable and now fails because the Parser was changed without it's knowledge.

    Only workaround this issue for now was to manully specify the unwrapper everywhere, which in turn eliminates the need for the global unwrapper in the first place. This is a great example of why you should not use a global variable.

    opened by dominik-hadl 2
  • Merge performance tests, move comparison tests to separate repository

    Merge performance tests, move comparison tests to separate repository

    All Serializable related performance tests should be merged into develop and master.

    Tests for comparing other frameworks against Serializable should be moved to a different repository, so that this one isn't cluttered.


    This will also make sure that performance tests are run in each travis build, which should make it easier for us to spot regressions or improvements.

    opened by dominik-hadl 2
  • Fix GitHub language detection by ignoring the Prebuild folder

    Fix GitHub language detection by ignoring the Prebuild folder

    Serializable is seen as a mainly Objective-C repo. Renaming the Prebuild folder to Dependencies should make it be ignored when analysing language, according to https://github.com/github/linguist/blob/master/lib/linguist/vendor.yml

    opened by mariusc 2
  • Adding support for Swift.Codable

    Adding support for Swift.Codable

    Add support for Swift.Codable that can be used with both Alamofire and URLSession

    https://trello.com/c/6AUldeXS/99-codable-vs-serpent https://trello.com/c/iqT6jV2t/107-8-alamofire-vs-urlsession

    Swift.Codable vs Serializable

    Using Swift.Codable reduces quite a bit of the boiler plate code needed by our data struct's in ordeer to comply to Serializable.

    With Swift.Codable, if the server response data contains matching keys with our variable names, the only code needed to conform to Swift.Codable is:

    import Foundation
    
    struct CategoryItem: Codable {
        var title = ""
        var id = ""
    }
    

    while if the variable names do not match conforming to Swift.Codable is done as follows:

    import Foundation
    
    struct CategoryItem: Codable {
        var title = ""
        var id = ""
            
        init(from decoder: Decoder) throws {
            let map = try decoder.container(keyedBy: CodingKeys.self)
            self.id = try map.decode(String.self, forKey: .id)
            self.title = try map.decode(String.self, forKey: .title)
        }
        
        private enum CodingKeys: String, CodingKey {
            case id
            case title
        }
    }
    

    when trying to Swift.Decode or Swift.Encode optionals we should do them by using try? instead of try :

    import Foundation
    
    struct CategoryItem: Codable {
        var title: String?
        var id = ""
           
        init(from decoder: Decoder) throws {
            let map = try decoder.container(keyedBy: CodingKeys.self)
            self.id = try map.decode(String.self, forKey: .id)
            self.title = try? map.decode(String.self, forKey: .title)
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode (id, forKey: .id)
            try? container.encode (title, forKey: .title)
        }
        
        private enum CodingKeys: String, CodingKey {
            case id
            case title
        }
    }
    

    As well we can use an Enum and conform it to Codable

    enum Type: String, Codable {}
    

    If needed Decoding an Enum can be done like this:

    enumVar =  try map.decode(Type.self, forKey: .enumVar)
    

    Swift.Codable and URLSession

    Opening a dataTask with URLSession and decoding the data can be done fairly easy now by doing the following:

    guard let urlComponent = urlComponent, let url = urlComponent.url?.appendingPathComponent("breaking") else { return } let dataTask = urlSession.dataTask(with: url, completionHandler: urlSession.decode(completion)) dataTask.resume()

    where urlComponent is of type URLComponents? and urlSession.decode(completion) is introduced in the upcoming Pull Request. As the completion parameter we can pass a DResult<Value>, where Value is a Generic Type and can contain stand alone objects or Arrays of objects.

    Also in order to facilitate the paginated requests there is added a DPaginatedResponse (DecodedPaginatedResponse) that can be added in the completion as follow: completion: @escaping (DResult<DPaginatedResponse<Value>>)

    Pull Requst: https://github.com/nodes-ios/Serpent/pull/167

    opened by kjoneandrei 0
  • Swift array with Typed Swift enum causes crash when Encoding

    Swift array with Typed Swift enum causes crash when Encoding

    enum Attribute: String { case one = "15" case two = "19" case three = "" }

    struct MyStructType { var myArray = [Attribute.one] }

    extension MyStructType: Serializable { init(dictionary: NSDictionary?) { myArray <== (self, dictionary, "myArray") }

    func encodableRepresentation() -> NSCoding {
        let dict = NSMutableDictionary()
        (dict, "myArray") <== myArray // myArray.map({ $0.encodableRepresentation() }) seems to fix it
        return dict
    }
    

    }

    func shouldTriggerCrash() { let theInstance = MyStructType() Cashier.defaultCache().setSerializable(theInstance, forKey: rideContainerCacheKey)//Crashes }

    opened by jakobmygind 0
  • Improve code documentation

    Improve code documentation

    I ran Jazzy locally on Serpent, and it looks like the documentation percentage is not very high. This is about documentation for functions that the users don't actually access, but still, for completeness and quality, we should document those too.

    enhancement 
    opened by mariusc 0
Releases(2.0.3)
🧱 A JSON decoding/encoding library that handles optimistically or strictly.

Do you need to handle the root cause of failure in decoding JSON? We often process the value as a default value if it could not be decoded from JSON.

Muukii 252 Oct 28, 2022
Codable code is a Swift Package that allows you to convert JSON Strings into Swift structs

Codable code is a Swift Package that allows you to convert JSON Strings into Swift structs.

Julio Cesar Guzman Villanueva 2 Oct 6, 2022
Implement dynamic JSON decoding within the constraints of Swift's sound type system by working on top of Swift's Codable implementations.

DynamicCodableKit DynamicCodableKit helps you to implement dynamic JSON decoding within the constraints of Swift's sound type system by working on top

SwiftyLab 15 Oct 16, 2022
Himotoki (紐解き) is a type-safe JSON decoding library written purely in Swift.

Himotoki Himotoki (紐解き) is a type-safe JSON decoding library written purely in Swift. This library is highly inspired by the popular Swift JSON parsin

IKEDA Sho 799 Dec 6, 2022
Nikolai Saganenko 1 Jan 9, 2022
JSONExport is a desktop application for Mac OS X which enables you to export JSON objects as model classes with their associated constructors, utility methods, setters and getters in your favorite language.

JSONExport JSONExport is a desktop application for Mac OS X written in Swift. Using JSONExport you will be able to: Convert any valid JSON object to a

Ahmed Ali 4.7k Dec 30, 2022
JSEN (JSON Swift Enum Notation) is a lightweight enum representation of a JSON, written in Swift.

JSEN /ˈdʒeɪsən/ JAY-sən JSEN (JSON Swift Enum Notation) is a lightweight enum representation of a JSON, written in Swift. A JSON, as defined in the EC

Roger Oba 8 Nov 22, 2022
Command line tool written in Swift dedicated to perform Mutation Testing of your Swift project

Mutanus Command line tool written in Swift dedicated to perform Mutation Testing of your Swift project. Inspired by Muter Usage mutanus -c <path-to-co

Iurii Sorokin 36 Sep 21, 2022
Swift-json - High-performance json parsing in swift

json 0.1.4 swift-json is a pure-Swift JSON parsing library designed for high-per

kelvin 43 Dec 15, 2022
Decodable Simple and strict, yet powerful object mapping made possible by Swift 2's error handling.

Decodable Simple and strict, yet powerful object mapping made possible by Swift 2's error handling. Greatly inspired by Argo, but without a bizillion

Johannes Lund 1k Jul 15, 2022
Elevate is a JSON parsing framework that leverages Swift to make parsing simple, reliable and composable

Elevate is a JSON parsing framework that leverages Swift to make parsing simple, reliable and composable. Elevate should no longer be used for

Nike Inc. 611 Oct 23, 2022
HandyJSON is a framework written in Swift which to make converting model objects to and from JSON easy on iOS.

HandyJSON To deal with crash on iOS 14 beta4 please try version 5.0.3-beta HandyJSON is a framework written in Swift which to make converting model ob

Alibaba 4.1k Dec 29, 2022
ObjectMapper is a framework written in Swift that makes it easy for you to convert your model objects to and from JSON.

ObjectMapper is a framework written in Swift that makes it easy for you to convert your model objects (classes and structs) to and from J

Tristan Himmelman 9k Jan 2, 2023
YamlSwift - Load YAML and JSON documents using Swift

YamlSwift parses a string of YAML document(s) (or a JSON document) and returns a Yaml enum value representing that string.

Behrang Norouzinia 384 Nov 11, 2022
Swift/Obj-C HTTP framework with a focus on REST and JSON

Now Archived and Forked PMHTTP will not be maintained in this repository going forward. Please use, create issues on, and make PRs to the fork of PHMT

Postmates Inc. 509 Sep 4, 2022
Swift parser for JSON Feed — a new format similar to RSS and Atom but in JSON.

JSONFeed Swift parser for JSON Feed — a new format similar to RSS and Atom but in JSON. For more information about this new feed format visit: https:/

Toto Tvalavadze 31 Nov 22, 2021
Jay - Pure-Swift JSON parser & formatter. Fully streamable input and output. Linux & OS X ready.

Pure-Swift JSON parser & formatter. Fully streamable input and output. Linux & OS X ready. Replacement for NSJSONSerialization.

Danielle 132 Dec 5, 2021
Encode and decode deeply-nested data into flat Swift objects

DeepCodable: Encode and decode deeply-nested data into flat Swift objects Have you ever gotten a response from an API that looked like this and wanted

Mike Lewis 91 Dec 26, 2022
AlamofireObjectMappe - An Alamofire extension which converts JSON response data into swift objects using ObjectMapper

AlamofireObjectMapper An extension to Alamofire which automatically converts JSON response data into swift objects using ObjectMapper. Usage Given a U

Tristan Himmelman 2.6k Dec 29, 2022