I have an API, written in Vapor. I would like to follow the JSON API Spec.
I am struggling with understanding how I can create my response object in the correct format.
For example, I would like my responses to be structured as so...
{
"links": {
"self": "http://example.com/dish",
"next": "http://example.com/dish?page=2",
"last": "http://example.com/dish?page=10"
},
"data": [{
"title": "Spag Bol",
"course": "main",
"description": "BasGetti",
"price": 3.9900000000000002
},
{
"title": "Ice Cream",
"course": "desert",
"description": "Vanilla",
"price": 0.98999999999999999
}]
}
I can return the contents of data quite simply if POST
to this endpoint (pseudo code)
router.post(Dish.self, at: "api/dish") { req, data -> Future<Dish> in
return Future.map(on: req, { () -> Dish in
data.id = 001
return data
})
}
I tried creating an ApiResponse
class and passing in the data so I could structure the response but this did not work with the error Cannot convert return expression of type 'ApiResonse' to return type 'Dish'
router.post(Dish.self, at: "api/dish") { req, data -> Future<Dish> in
return Future.map(on: req, { () -> Dish in
data.id = 001
return ApiResonse(links: Links(self: "http://google.com", next: "http://google.com", last: "http://google.com"), data: data)
})
}
I am not sure how I can do this. These are the attempted classes
final class Dish: Content {
var id: Int?
var title: String
var description: String
var course: String
var price: Double
init(title: String, description: String, course: String, price: Double) {
self.title = title
self.description = description
self.course = course
self.price = price
}
}
struct Links {
var `self`: String?
var next: String?
var last: String?
}
class ApiResonse {
var links: Links?
var data: Any
init(links: Links, data: Any) {
self.links = links
self.data = data
}
}
Do I need to use Generics to set up the response class? Is anyone able to provide an example?
Each class
or struct
in the compound object ApiResponse
needs to comply with the Content
protocol. The Content
protocol includes the Codable
protocol for JSON decoding and encoding.
Note that Any
does not comply with the Codable
protocol, and therefore Any
can not be used as any component part of a Response. See Vapor 3 Docs: "Using Content" and Vapor 4 Docs: "Content" for more detailed information.
Vapor 3: all content types (JSON, protobuf, URLEncodedForm, Multipart, etc) are treated the same. All you need to parse and serialize content is a
Codable
class or struct.
Vapor 4: Vapor's content API allows you to easily encode / decode
Codable
structs to / from HTTP messages.
An object or compound object which fully complies with Content
can be used as a ResponseEncodable
response.
The ApiResponse
model can be generic when each route endpoint resolves to a specific Content
protocol compliant type.
An example project with the code below is on GitHub: VaporExamplesLab/Example-SO-VaporJsonResponse.
Example Models
struct Dish: Content {
var id: Int?
var title: String
var description: String
var course: String
var price: Double
init(id: Int? = nil, title: String, description: String, course: String, price: Double) {
self.id = id
self.title = title
self.description = description
self.course = course
self.price = price
}
}
struct Links: Content {
var current: String?
var next: String?
var last: String?
}
struct ApiResponse: Content {
var links: Links?
var dishes: [Dish]
init(links: Links, dishes: [Dish]) {
self.links = links
self.dishes = dishes
}
}
Example POST
: Returns ApiResponse
router.post(Dish.self, at: "api/dish") {
(request: Request, dish: Dish) -> ApiResponse in
var dishMutable = dish
dishMutable.id = 001
var links = Links()
links.current = "http://example.com"
links.next = "http://example.com"
links.last = "http://example.com"
return ApiResponse(links: links, dishes: [dishMutable])
}
Example POST
: Returns Future<ApiResponse>
router.post(Dish.self, at: "api/dish-future") {
(request: Request, dish: Dish) -> Future<ApiResponse> in
var dishMutable = dish
dishMutable.id = 002
var links = Links()
links.current = "http://example.com"
links.next = "http://example.com"
links.last = "http://example.com"
return Future.map(on: request, {
() -> ApiResponse in
return ApiResponse(links: links, dishes: [dishMutable])
})
}
JSON Response Received
The Response body for the above code produces the following:
{
"links": {
"current": "http://example.com",
"next": "http://example.com",
"last": "http://example.com"
},
"dishes": [
{
"id": 1,
"title": "Aztec Salad",
"description": "Flavorful Southwestern ethos with sweet potatos and black beans.",
"course": "salad",
"price": 1.82
}
]
}
Generic Model
struct ApiResponseGeneric<T> : Content where T: Content {
var links: Links?
var data: T
init(links: Links, data: T) {
self.links = links
self.data = data
}
}
Concrete Route Endpoint
router.post(Dish.self, at: "api/dish-generic-future") {
(request: Request, dish: Dish) -> Future<ApiResponseGeneric<[Dish]>> in
var dishMutable = dish
dishMutable.id = 004
var links = Links()
links.current = "http://example.com"
links.next = "http://example.com"
links.last = "http://example.com"
return Future.map(on: request, {
() -> ApiResponseGeneric<[Dish]> in
return ApiResponseGeneric<[Dish]>(links: links, data: [dishMutable])
})
}
You need to have data
be [Dish]
class ApiResonse {
var links: Links?
var data: [Dish]
init(links: Links, data: [Dish]) {
self.links = links
self.data = [Dish]
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With