Vox
Swift JSON:API client framework
Install / Use
/learn @aronbalog/VoxREADME
Vox
Vox is a Swift JSONAPI standard implementation. 🔌
🔜 More stable version (written in Swift 5) coming soon.
- 🎩 The magic behind
- 💻 Installation
- 🚀 Usage
- Defining resource
- Serializing
- Deserializing
- Networking
- Client protocol
- Alamofire client plugin
- Fetching single resource
- Fetching resource collection
- Creating resource
- Updating resource
- Deleting resource
- Pagination
- Custom routing
- ✅ Tests
- Contributing
- License
The magic behind
Vox combines Swift with Objective-C dynamism and C selectors. During serialization and deserialization JSON is not mapped to resource object(s). Instead, it uses Marshalling and Unmarshalling techniques to deal with direct memory access and performance challenges. Proxy (surrogate) design pattern gives us an opportunity to manipulate JSON's value directly through class properties and vice versa.
import Vox
class Person: Resource {
@objc dynamic var name: String?
}
let person = Person()
person.name = "Sherlock Holmes"
print(person.attributes?["name"]) // -> "Sherlock Holmes"
Let's explain what's going on under the hood!
-
Setting the person's name won't assign the value to
Personobject. Instead it will directly mutate the JSON behind (the one received from server). -
Getting the property will actually resolve the value in JSON (it points to its actual memory address).
-
When values in resource's
attributesorrelationshipdictionaries are directly changed, getting the property value will resolve to the one changed in JSON.
Every attribute or relationship (Resource subclass property) must have @objc dynamic prefix to be able to do so.
Think about your
Resourceclasses as strong typed interfaces to a JSON object.
This opens up the possibility to easily handle the cases with:
- I/O performance
- polymorphic relationships
- relationships with circular references
- lazy loading resources from includes list
Installation
Requirements
- Xcode 9
- Cocoapods
Basic
pod 'Vox'
With Alamofire plugin
pod 'Vox/Alamofire'
Usage
Defining resource
import Vox
class Article: Resource {
/*--------------- Attributes ---------------*/
@objc dynamic
var title: String?
@objc dynamic
var descriptionText: String?
@objc dynamic
var keywords: [String]?
@objc dynamic
var viewsCount: NSNumber?
@objc dynamic
var isFeatured: NSNumber?
@objc dynamic
var customObject: [String: Any]?
/*------------- Relationships -------------*/
@objc dynamic
var authors: [Person]?
@objc dynamic
var editor: Person?
/*------------- Resource type -------------*/
// resource type must be defined
override class var resourceType: String {
return "articles"
}
/*------------- Custom coding -------------*/
override class var codingKeys: [String : String] {
return [
"descriptionText": "description"
]
}
}
Serializing
Single resource
import Vox
let person = Person()
person.name = "John Doe"
person.age = .null
person.gender = "male"
person.favoriteArticle = .null()
let json: [String: Any] = try! person.documentDictionary()
// or if `Data` is needed
let data: Data = try! person.documentData()
Previous example will resolve to following JSON:
{
"data": {
"attributes": {
"name": "John Doe",
"age": null,
"gender": "male"
},
"type": "persons",
"id": "id-1",
"relationships": {
"favoriteArticle": {
"data": null
}
}
}
}
In this example favorite article is unassigned from person. To do so, use .null() on resource properties and .null on all other properties.
Resource collection
import Vox
let article = Article()
article.id = "article-identifier"
let person1 = Person()
person1.id = "id-1"
person1.name = "John Doe"
person1.age = .null
person1.gender = "male"
person1.favoriteArticle = article
let person2 = Person()
person2.id = "id-2"
person2.name = "Mr. Nobody"
person2.age = 99
person2.gender = .null
person2.favoriteArticle = .null()
let json: [String: Any] = try! [person1, person2].documentDictionary()
// or if `Data` is needed
let data: Data = try! [person1, person2].documentData()
Previous example will resolve to following JSON:
{
"data": [
{
"attributes": {
"name": "John Doe",
"age": null,
"gender": "male"
},
"type": "persons",
"id": "id-1",
"relationships": {
"favoriteArticle": {
"data": {
"id": "article-identifier",
"type": "articles"
}
}
}
},
{
"attributes": {
"name": "Mr. Nobody",
"age": 99,
"gender": null
},
"type": "persons",
"id": "id-2",
"relationships": {
"favoriteArticle": {
"data": null
}
}
}
]
}
Nullability
Use .null() on Resource type properties or .null on any other type properties.
- Setting property value to
.null(or.null()) will result in JSON value being set tonull - Setting property value to
nilwill remove value from JSON
Deserializing
Single resource
import Vox
let data: Data // -> provide data received from JSONAPI server
let deserializer = Deserializer.Single<Article>()
do {
let document = try deserializer.deserialize(data: self.data)
// `document.data` is an Article object
} catch JSONAPIError.API(let errors) {
// API response is valid JSONAPI error document
errors.forEach { error in
print(error.title, error.detail)
}
} catch JSONAPIError.serialization {
print("Given data is not valid JSONAPI document")
} catch {
print("Something went wrong. Maybe `data` does not contain valid JSON?")
}
Resource collection
import Vox
let data: Data // -> provide data received from JSONAPI server
let deserializer = Deserializer.Collection<Article>()
let document = try! deserializer.deserialize(data: self.data)
// `document.data` is an [Article] object
Description
Provided data must be Data object containing valid JSONAPI document or error. If this preconditions are not met, JSONAPIError.serialization error will be thrown.
Deserializer can also be declared without generic parameter but in that case the resource's data property may need an enforced casting on your side so using generics is recommended.
Document<DataType: Any> has following properties:
| Property | Type | Description |
|:--------------- |:------------------|:------------------------------------------|
| data | DataType | Contains the single resource or resource collection
| meta | [String: Any] | meta dictionary
| jsonapi | [String: Any] | jsonApi dictionary
| links | Links | Links object, e.g. can contain pagination data
| included | [[String: Any]] | included array of dictionaries
Networking
<id> and <type> annotations can be used in path strings. If possible, they'll get replaced with adequate values.
Client protocol
Implement following method from Client protocol:
func executeRequest(path: String,
method: String,
queryItems: [URLQueryItem],
bodyParameters: [String : Any]?,
success: @escaping ClientSuccessBlock,
