Nitzan Ohana
Nitzan Ohana's Blog

Nitzan Ohana's Blog

How to add generic pagination to swagger

How to add generic pagination to swagger

Nitzan Ohana's photo
Nitzan Ohana

Published on Sep 11, 2021

6 min read

In this article you'll learn how to design and implement a reusable swagger pagination solution to any RESTful API.

I assume you already know what swagger is, if not - read about it here.

And if you want to skip right to the solution have a look at the code or check out the live demo.

Our Example API - Dogs and Cats

Schemas

yan-laurichesse-3qZnN_M45Ds-unsplash.jpg Fig.1 - two YAML objects

First, we'll define two schemas:

Dog schema which represents a dog:

components:
  schemas:
    Dog:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        ownerName: { type: string }
        favoriteToy: { type: string }

And Cat schema which represents a cat:

components:
  schemas:
    Cat:
      type: object
        id: { type: integer }
        name: { type: string }
        servantName: { type: string }
        favoriteFish: { type: string }

Endpoints

We also define two endpoints which returns lists all the dogs and cats in our database:

  1. GET /dogs - returns an array of all the Dog schemas
  2. GET /cats - returns an array of all the Cat schemas

In swagger:

paths:
    /dogs:
    get:
      tags:
        - dog
      summary: Find all dogs
      responses:
        200:
          description: success
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Dog'   
    /cats:
    get:
      tags:
        - cats
      summary: Find all cats
      responses:
        200:
          description: success
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Cat'

For example, if we call GET /dogs we'll receive the following response from the api:

[
  { "id": 1, "name": "Spike", "ownerName": "Dave", "favoriteToy": "Ball"},
  { "id": 2, "name": "Lenny", "ownerName": "Michelle", "favoriteToy": "Rubber bone" },
  { "id": 3, "name": "Spike", "ownerName": "Ron","favoriteToy": "Ron's shoes"}
]

Oh no, too many dogs

judi-neumeyer-ECjHeJtRznQ-unsplash.jpg Fig.1 - GET /dogs with at least 5 results

But what happens if we return 100, 5000 or a million dogs in every GET /dogs HTTP request? that's really bad for performance and takes a lot of time and resources.

To solve that, instead of returning an array of Dog schemas, we'll use pagination:

  1. To every endpoint we'll add a page and per_page query string parameters to indicate which page we need and how many results we want to be in every page returned.
  2. We'll change our backend code of GET /dogs to return paginated result so that each call represents a single page from the list of all the items available in the db.

For example for GET /dogs?page=3&per_page=20 we'll receive:

{
  "has_next": true,    // do we have a another page after?
  "has_prev": true,   // do we have a page before?
  "total": 100,        // how many dogs we have in total in the db
  "page": 3,           // current page
  "per_page": 20,      // how many items per page the user requested
  "pages": 5,          // total number of pages
  "results": [         // the results (20 in this case)
             ... 
             { "id": 64, "name": "Rocky", "ownerName": "Lenny", "favoriteToy": "Ball"},
             ...
           ]
}

So now in swagger our get /dogs endpoint will be:

paths:
    /dogs:
    get:
      tags:
        - dog
      summary: Find all dogs
      responses:
        200:
          description: success
          content:
            application/json:
              schema:
                type: object
                properties:
                  total: { type: number }
                  page: { type: number }
                  per_page: { type: number }
                  has_next: { type: bool }
                  has_prev: { type: bool }
                  results: 
                    type: array           
                    items:
                      $ref: '#/components/schemas/Dog'

But what about the cats?

pexels-tranmautritam-2194261.jpg Fig.2 - Cat without pagination support

If you remember we have another endpoint - GET /cats which now needs to be paginated as well, so we'll add pagination in the backend and change the cats endpoints to add paginated result as well:

paths:
    /cats:
    get:
      tags:
        - cat
      summary: Find all cats
      responses:
        200:
          description: success
          content:
            application/json:
              schema:
                type: object
                properties:
                  total: { type: number }        // duplicate definition
                  page: { type: number }         // duplicate definition
                  per_page: { type: number }     // duplicate definition
                  has_next: { type: bool }       // duplicate definition
                  has_prev: { type: bool }       // duplicate definition 
                  results: 
                    type: array           
                    items:
                      $ref: '#/components/schemas/Cat'

This causes two issues:

  1. We need to repeat the same fields definition (total, per_page, page, ...) in all of our endpoints, this both duplicates code and prone to errors.
  2. If in the future we'll want to change anything in the pagination schema (for example renaming total field to totalResults or adding a new field) we'll have to do that for ALL the endpoints in our API manually.

So instead of repeating this structure in our endpoints we'll create a generic and reusable pagination schema that can be re-used across all the endpoints

How? keep reading

Generic pagination solution

We'll start by defining a generic PaginationResult schema:

components:
  schemas:
    PaginatedResult:
      type: object
      properties:
        total: { type: number }
        page: { type: number }
        per_page: { type: number }
        has_next: { type: bool }
        has_prev: { type: bool }
        results: { type: array, items: {} }  #  any type of items

According to swagger's array documentation when we define an array items with {} type, it means an array with arbitrary types (any kind)

Which is exactly what we need because every endpoint has it's own results item's type in it. Now all that's left is to combine the PaginatedResult schema with the correct schema (Dog, Cat) in every endpoint.

This is done by using swagger's allOf keywords which combines several schemas together.

From swagger's documentation:

OpenAPI lets you combine and extend model definitions using the allOf keyword. allOf takes an array of object definitions that are used for independent validation but together compose a single object

We'll use it to combine two schemas for every endpoint:

  1. GET /dogs = PaginatedResult + Dog
  2. GET /cats = PaginatedResult + Cat

And now we'll receive a custom pagination solution for all the endpoints:

paths:
  /dogs:
    get:
      tags:
        - dog
      summary: Find all dogs
      parameters:
        - { in: query, name: page, schema: { type: integer } }
        - { in: query, name: per_page, schema: { type: integer } }
      responses:
        200:
          description: success
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResult"
                  - type: object
                    properties:
                      results:
                        type: array
                        items:
                          $ref: "#/components/schemas/Dog"
  /cats:
    get:
      tags:
        - cat
      summary: Find all cats
      parameters:
        - { in: query, name: page, schema: { type: integer } }
        - { in: query, name: per_page, schema: { type: integer } }
      responses:
        200:
          description: success
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/PaginatedResult"
                  - type: object
                    properties:
                      results:
                        type: array
                        items:
                          $ref: "#/components/schemas/Cat"

Demo

To make sure this actually works I've created a small swagger website with the full example,

  1. Browse to it here: https://nitzano.github.io/pagination-swagger
  2. And The full code can be found in this github repo: https://github.com/nitzano/pagination-swagger

That's all, Hope you had fun and keep documenting!

 
Share this