Search code examples
node.jsswaggerhapi.jsjoihapi-swagger

Is there a way to define an integer property as int64 in a Joi model that is used by Hapi Swagger


I am having the following Joi model

const SimpleModel = Joi.object({
    id: Joi.number().integer().required().description('ID')
}).label('SimpleModel');

The model is being used in the @hapi/hapi route below

{
    method: 'GET',
    path: `/api/v1.0/data`,
    handler: () => { id: 1 }, // a mock handler
    config: {
        plugins: {
            'hapi-swagger': {
                responses: {
                    200: {
                        description: 'OK',
                        schema: SimpleModel
                    }
                }
            }
        }
    }
}

The above model is generated as shown the below swagger definition

swagger: '2.0'
host: localhost:8080
basePath: /api
info: 
  title: 'Swagger for SimpleModel'
  version: '1.0'
schemes:
  - http
  - https
paths:
  /v1.0/data:
    get:
      summary: Returns an object
      responses:
        '200':
          schema:
            $ref: '#/definitions/SimpleModel'
          description: OK
definitions:
  SimpleModel:
    type: object
    properties:
      id:
        type: integer
        description: ID
    required:
      - id

What I want is to add an additional field to id, which is format: int64, e.g.

definitions:
  SimpleModel:
    type: object
    properties:
      id:
        type: integer
        format: int64 # <-- new field here!
        description: ID
    required:
      - id

While this is supported by swagger, I cannot find any way to define it in my Joi model so that it will appear in the generated swagger by hapi-swagger.

I am searching online for days but I haven't been able to find anything helpful on the web, either a documentation or an example.

Is there a way to include format: int64 in the generated swagger of SimpleModel?


Solution

  • package.json

    {
      "name": "71422008",
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "dependencies": {
        "@hapi/hapi": "^20.2.1",
        "@hapi/inert": "^6.0.5",
        "@hapi/vision": "^6.1.0",
        "blipp": "^4.0.2",
        "hapi-swagger": "^14.2.5",
        "joi": "^17.6.0",
        "qs": "^6.10.3"
      }
    }
    

    index.js

    const Hapi = require('@hapi/hapi');
    const Joi = require('joi');
    const Blipp = require('blipp');
    const Inert = require('@hapi/inert');
    const Vision = require('@hapi/vision');
    const HapiSwagger = require('hapi-swagger');
    const HapiSwaggerProperties = require('hapi-swagger/lib/properties');
    
    const _parseNumber = HapiSwaggerProperties.prototype.parseNumber;
    HapiSwaggerProperties.prototype.parseNumber = function(property, joiObj) {
      let _property = _parseNumber.apply(this, [property, joiObj]);
      _property.format = 'int64';
      return _property;
    }
    const SimpleModel = Joi.object({
        id: Joi.number().integer().required().description('ID')
    }).label('SimpleModel');
    
    const ser = async () => {
      const swaggerOptions = {
        info: {
          title: 'Test API Documentation',
          description: 'This is a sample example of API documentation.'
        }
      };
    
      const server = Hapi.Server({
        host: 'localhost',
        port: 3000
      });
    
      await server.register([
        Inert,
        Vision,
        Blipp,
        {
          plugin: HapiSwagger,
          options: swaggerOptions
        }
      ]);
    
      server.route({
        method: 'GET',
        path: `/api/v1.0/data`,
        options: {
          handler: () => { id: 1 }, // a mock handler
          tags: ['api'],
          plugins: {
            'hapi-swagger': {
              responses: {
                200: {
                  description: 'OK',
                  schema: SimpleModel
                }
              }
            }
          }
        },
      });
    
      await server.start();
      return server;
    };
    
    ser()
      .then((server) => {
        console.log(`Server listening on ${server.info.uri}`);
      })
      .catch((err) => {
        console.error(err);
        process.exit(1);
      });
    

    Output:

    ...
        "definitions": {
            "SimpleModel": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer",
                        "description": "ID",
                        "format": "int64"
                    }
                },
                "required": [
                    "id"
                ]
            }
        }
    ...
    

    Example 2 with custom types:

    const Hapi = require('@hapi/hapi');
    const Joi = require('joi');
    const Blipp = require('blipp');
    const Inert = require('@hapi/inert');
    const Vision = require('@hapi/vision');
    const HapiSwagger = require('hapi-swagger');
    const HapiSwaggerProperties = require('hapi-swagger/lib/properties');
    
    const custom = Joi.extend(joi => ({
      type: 'number',
      base: Joi.number().$_setFlag('type', 'integer').$_setFlag('format', 'int64'),
      rules: {
        i32: {
          method(first, second) {
            return this.$_setFlag('type', 'integer').$_setFlag('format', 'int32');
          }
        }
      }
    }));
    
    const _parseNumber = HapiSwaggerProperties.prototype.parseNumber;
    HapiSwaggerProperties.prototype.parseNumber = function(property, joiObj) {
      let _property = _parseNumber.apply(this, [property, joiObj]);
    
      const describe = joiObj.describe();
      if (describe.flags) {
        if (describe.flags.format) {
          _property.format = describe.flags.format;
        }
    
        if (describe.flags.type) {
          _property.type = describe.flags.type;
        }
      }
      return _property;
    }
    
    const SimpleModel = Joi.object({
        id: Joi.number().integer().required().description('ID'),
        seconds: custom.number().i32(),
        epochSeconds: custom.number(),
    }).label('SimpleModel');
    
    const ser = async () => {
      const swaggerOptions = {
        info: {
          title: 'Test API Documentation',
          description: 'This is a sample example of API documentation.'
        }
      };
    
      const server = Hapi.Server({
        host: 'localhost',
        port: 3000
      });
    
      await server.register([
        Inert,
        Vision,
        Blipp,
        {
          plugin: HapiSwagger,
          options: swaggerOptions
        }
      ]);
    
      server.route({
        method: 'GET',
        path: `/api/v1.0/data`,
        options: {
          handler: () => { id: 1 }, // a mock handler
          tags: ['api'],
          plugins: {
            'hapi-swagger': {
              responses: {
                200: {
                  description: 'OK',
                  schema: SimpleModel
                }
              }
            }
          }
        },
      });
    
      await server.start();
      return server;
    };
    
    ser()
      .then((server) => {
        console.log(`Server listening on ${server.info.uri}`);
      })
      .catch((err) => {
        console.error(err);
        process.exit(1);
      });
    

    Output:

    ...
        "definitions": {
            "SimpleModel": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer",
                        "description": "ID"
                    },
                    "seconds": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "epochSeconds": {
                        "type": "integer",
                        "format": "int64"
                    }
                },
                "required": [
                    "id"
                ]
            }
        }
    ...
    

    Example 3 with custom types and meta properties:

    const Hapi = require('@hapi/hapi');
    const Hoek = require('@hapi/hoek');
    const Joi = require('joi');
    const Blipp = require('blipp');
    const Inert = require('@hapi/inert');
    const Vision = require('@hapi/vision');
    const HapiSwagger = require('hapi-swagger');
    const HapiSwaggerProperties = require('hapi-swagger/lib/properties');
    
    const custom = Joi.extend(joi => ({
      type: 'int',
      base: Joi.number().meta({
        'type': 'integer',
        'format': 'int32',
      }),
      rules: {
        i64: {
          method(first, second) {
            return this.meta({
              'type': 'integer',
              'format': 'int64',
            });
          }
        }
      }
    }));
    
    const _parsePropertyMetadata = HapiSwaggerProperties.prototype.parsePropertyMetadata;
    HapiSwaggerProperties.prototype.parsePropertyMetadata = function(property, name, parent, joiObj) {
      let _property = _parsePropertyMetadata.apply(this, [property, name, parent, joiObj]);
    
      const _type = Hoek.reach(joiObj, 'type');
      if (!this.propertyMap[_type]) {
        delete _property['x-meta']
      }
    
      let xMeta = Hoek.reach(joiObj, '$_terms.metas');
      xMeta = xMeta.length > 0 ? xMeta[xMeta.length - 1] : undefined;
      if (xMeta) {
        if (xMeta.type) {
          _property.type = xMeta.type;
        }
    
        if (xMeta.format) {
          _property.format = xMeta.format;
        }
      }
    
      return _property;
    }
    
    const SimpleModel = Joi.object({
        id: Joi.number().integer().required().description('ID'),
        seconds: custom.int(),
        epochSeconds: custom.int().i64(),
    }).label('SimpleModel');
    
    const ser = async () => {
      const swaggerOptions = {
        info: {
          title: 'Test API Documentation',
          description: 'This is a sample example of API documentation.'
        }
      };
    
      const server = Hapi.Server({
        host: 'localhost',
        port: 3000
      });
    
      await server.register([
        Inert,
        Vision,
        Blipp,
        {
          plugin: HapiSwagger,
          options: swaggerOptions
        }
      ]);
    
      server.route({
        method: 'GET',
        path: `/api/v1.0/data`,
        options: {
          handler: () => { id: 1 }, // a mock handler
          tags: ['api'],
          plugins: {
            'hapi-swagger': {
              responses: {
                200: {
                  description: 'OK',
                  schema: SimpleModel
                }
              }
            }
          }
        },
      });
    
      await server.start();
      return server;
    };
    
    ser()
      .then((server) => {
        console.log(`Server listening on ${server.info.uri}`);
      })
      .catch((err) => {
        console.error(err);
        process.exit(1);
      });
    
    ...
        "definitions": {
            "SimpleModel": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "integer",
                        "description": "ID"
                    },
                    "seconds": {
                        "type": "integer",
                        "format": "int32"
                    },
                    "epochSeconds": {
                        "type": "integer",
                        "format": "int64"
                    }
                },
                "required": [
                    "id"
                ]
            }
        }
    ...