We are about to walk through a basic example to show how easy it is to set up a simple model. Information on creating models that take advantage of more of the features from the JSON:API Specification can be found in the README.
The JSONAPI framework relies heavily on generic types so the first step will
be to alias away some of the JSON:API features we do not need for our simple
example.
/// Our Resource objects will not have any metadata or links and they will be identified by Strings.
typealias Resource<Description: JSONAPI.ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>
/// Our JSON:API Documents will similarly have no metadata or links associated with them. Additionally, there will be no included resources.
typealias SingleDocument<Resource: ResourceObjectType> = JSONAPI.Document<SingleResourceBody<Resource>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
typealias BatchDocument<Resource: ResourceObjectType> = JSONAPI.Document<ManyResourceBody<Resource>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>The next step is to create ResourceObjectDescriptions and ResourceObjects.
For our simple example, let's create a Person and a Dog.
struct PersonDescription: ResourceObjectDescription {
// by common convention, we will use the plural form
// of the noun as the JSON:API "type"
static let jsonType: String = "people"
struct Attributes: JSONAPI.Attributes {
let firstName: Attribute<String>
let lastName: Attribute<String>
// we mark this attribute as "nullable" because the user can choose
// not to specify an age if they would like to.
let age: Attribute<Int?>
}
struct Relationships: JSONAPI.Relationships {
// we will define "Dog" next
let pets: ToManyRelationship<Dog, NoIdMetadata, NoMetadata, NoLinks>
}
}
// this typealias is optional, but it makes working with resource objects much
// more user friendly.
typealias Person = Resource<PersonDescription>
struct DogDescription: ResourceObjectDescription {
static let jsonType: String = "dogs"
struct Attributes: JSONAPI.Attributes {
let name: Attribute<String>
}
// we could relate dogs back to their owners, but for the sake of this example
// we will instead show how you would create a resource with no relationships.
typealias Relationships = NoRelationships
}
typealias Dog = Resource<DogDescription>At this point we have two objects that can decode JSON:API responses. To illustrate we can mock up a dog response and parse it.
// snag Foundation for JSONDecoder
import Foundation
let mockBatchDogResponse =
"""
{
"data": [
{
"type": "dogs",
"id": "123",
"attributes": {
"name": "Sparky"
}
},
{
"type": "dogs",
"id": "456",
"attributes": {
"name": "Charlie Dog"
}
}
]
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let dogsDocument = try! decoder.decode(BatchDocument<Dog>.self, from: mockBatchDogResponse)
let dogs = dogsDocument.body.primaryResource!.values
print("dogs parsed: \(dogs.count ?? 0)")To illustrate ResourceObject property access, we can loop over the dogs and
print their names.
for dog in dogs {
print(dog.name)
}Now let's parse a mocked Person response.
let mockSinglePersonResponse =
"""
{
"data": {
"type": "people",
"id": "88223",
"attributes": {
"first_name": "Lisa",
"last_name": "Offenbrook",
"age": null
},
"relationships": {
"pets": {
"data": [
{
"type": "dogs",
"id": "123"
},
{
"type": "dogs",
"id": "456"
}
]
}
}
}
}
""".data(using: .utf8)!
decoder.keyDecodingStrategy = .convertFromSnakeCase
let personDocument = try! decoder.decode(SingleDocument<Person>.self, from: mockSinglePersonResponse)Our Person object has both attributes and relationships. Generally what we care
about when accessing relationships is actually the Id(s) of the resource(s); the
loop below shows off how to access those Ids.
let person = personDocument.body.primaryResource!.value
let relatedDogIds = person ~> \.pets
print("related dog Ids: \(relatedDogIds)")To wrap things up, let's throw our dog resources into a local cache and tie things together a bit. There are many ways to go about storing or caching resources clientside. For additional examples of more robust solutions, take a look at JSONAPI-ResourceStorage.
let dogCache = Dictionary(uniqueKeysWithValues: zip(dogs.map { $0.id }, dogs))
func cachedDog(_ id: Dog.Id) -> Dog? { return dogCache[id] }
print("Our person's name is \(person.firstName) \(person.lastName).")
print("They have \((person ~> \.pets).count) pets named \((person ~> \.pets).compactMap(cachedDog).map { $0.name }.joined(separator: " and ")).")