Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making Vapor API response JSON API Spec compliant

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?

like image 850
Harry Blue Avatar asked Sep 08 '18 06:09

Harry Blue


2 Answers

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

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

  3. An object or compound object which fully complies with Content can be used as a ResponseEncodable response.

  4. 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])
    }) 
}
like image 119
l --marc l Avatar answered Sep 20 '22 01:09

l --marc l


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]
    }
}
like image 38
Andrew Edwards Avatar answered Sep 20 '22 01:09

Andrew Edwards