SkillAgentSearch skills...

Vox

Swift JSON:API client framework

Install / Use

/learn @aronbalog/Vox

README

Vox

Vox is a Swift JSONAPI standard implementation. 🔌

🔜 More stable version (written in Swift 5) coming soon.

Build Status codecov Platform CocoaPods Compatible

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 Person object. 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 attributes or relationship dictionaries 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 Resource classes 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 to null
  • Setting property value to nil will 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,
              
View on GitHub
GitHub Stars47
CategoryDevelopment
Updated1y ago
Forks18

Languages

Swift

Security Score

80/100

Audited on Feb 28, 2025

No findings