Repository: glennjones/hapi-swagger
Pull request #709 (2021-04-17 22:26:49)

Fix swagger validation errors
A set of fixes for #707 

smgn: This is likely not the best way to fix these issues, but something that works. Let me know your thoughts on this.

Patch:
diff --git a/lib/parameters.js b/lib/parameters.js
index 5e3df0d..d46466b 100644
--- a/lib/parameters.js
+++ b/lib/parameters.js
@@ -33,6 +33,8 @@ parameters.allowedProps = [
 // add none swagger property needed to flag touched state of required property
 parameters.allowedProps.push('optional');
 
+parameters.itemsObjectProperties = ['type', 'format', 'items', 'collectionFormat', 'default', 'maximum', 'exclusiveMaximum', 'minimum', 'exclusiveMinimum', 'maxLength', 'minLength', 'pattern', 'maxItems', 'minItems', 'uniqueItems', 'enum', 'multipleOf'];
+
 /**
  * takes a swagger schema object and returns a parameters object
  *
@@ -92,6 +94,7 @@ parameters.fromProperties = function(schemaObj, parameterType) {
       item.in = parameterType;
 
       // reinstate required at parameter level
+      delete item.required;
       if (schemaObj.required && (schemaObj.properties[key].required || schemaObj.required.indexOf(key) > -1)) {
         item.required = true;
       }
@@ -99,9 +102,31 @@ parameters.fromProperties = function(schemaObj, parameterType) {
         item.required = false;
       }
 
-      item = Utilities.removeProps(item, this.allowedProps);
+      // object is not a valid parameter type in v2.0, replacing it with string
+      this.replaceObjectsWithStrings(item);
+      
+      item = this.removePropsRecursively(item);
       out.push(item);
     });
   }
   return out;
 };
+
+parameters.replaceObjectsWithStrings = function (item) {
+  if(item.type === 'object'){
+    item.type = 'string';
+    item['x-type'] = 'object';
+    Utilities.findAndRenameKey(item, 'properties', 'x-properties')
+  }
+  if(item.items){ // repeat for array items
+    this.replaceObjectsWithStrings(item.items);
+  }
+}
+
+parameters.removePropsRecursively = function (item, props=this.allowedProps){
+  item = Utilities.removeProps(item, props);
+  if(item.items){ // repeat for array items
+    this.removePropsRecursively(item.items, this.itemsObjectProperties)
+  }
+  return item;
+}
diff --git a/lib/properties.js b/lib/properties.js
index c5cb12a..ecdecbe 100644
--- a/lib/properties.js
+++ b/lib/properties.js
@@ -166,7 +166,6 @@ internals.properties.prototype.parseProperty = function(name, joiObj, parent, pa
             ? objectPattern.schema.$_terms.examples[0]
             : objectPattern.schema.type;
       }
-      property.name = name;
       property.properties = {
         [patternName]: this.parseProperty(
           patternName,
@@ -434,7 +433,6 @@ internals.properties.prototype.parseObject = function(property, joiObj, name, pa
       property.optional = Utilities.replaceValue(property.optional, itemName, keyName);
     }
   });
-  property.name = name;
   return property;
 };
 


Test patch:
diff --git a/test/Integration/lout-test.js b/test/Integration/lout-test.js
index 04e8b1c..727f08d 100644
--- a/test/Integration/lout-test.js
+++ b/test/Integration/lout-test.js
@@ -2,6 +2,7 @@ const Code = require('@hapi/code');
 const Joi = require('joi');
 const Lab = require('@hapi/lab');
 const Helper = require('../helper.js');
+const SwaggerParser = require('swagger-parser');
 
 const expect = Code.expect;
 const lab = (exports.lab = Lab.script());
@@ -742,5 +743,8 @@ lab.experiment('lout examples', () => {
     expect(response.statusCode).to.equal(200);
     // the 40 to 45 difference is in one route having a number of methods
     // expect(response.result.paths).to.have.length(40);
+    
+    // console.log(JSON.stringify(response.result));
+    await SwaggerParser.validate(response.result);
   });
 });
diff --git a/test/Integration/query-test.js b/test/Integration/query-test.js
new file mode 100644
index 0000000..6e67cc4
--- /dev/null
+++ b/test/Integration/query-test.js
@@ -0,0 +1,199 @@
+const Code = require('@hapi/code');
+const Joi = require('joi');
+const Lab = require('@hapi/lab');
+const Helper = require('../helper.js');
+const Validate = require('../../lib/validate.js');
+
+const expect = Code.expect;
+const lab = (exports.lab = Lab.script());
+
+lab.experiment('query', () => {
+  lab.test('parameter required', async () => {
+    let testRoutes = [{
+      method: 'GET',
+      path: '/required',
+      options: {
+        handler: () => {},
+        tags: ['api'],
+        validate: {
+          query: Joi.object({
+            requiredParameter: Joi.string().required(),
+            existParameter: Joi.string().exist(),
+            defaultParameter: Joi.string(),
+            optionalParameter: Joi.string().optional()
+          })        
+        }
+      }
+    },
+    {
+      method: 'GET',
+      path: '/altParam',
+      options: {
+        handler: () => {},
+        tags: ['api'],
+        validate: {
+          query: Joi.object({
+            altParam: Joi.alternatives().try(Joi.number().required(), Joi.string())
+          })       
+        }
+      }
+    }];
+
+    const server = await Helper.createServer({}, testRoutes);
+    const response = await server.inject({ method: 'GET', url: '/swagger.json' });
+    expect(response.statusCode).to.equal(200);
+    expect(response.result.paths['/required'].get.parameters).to.equal([
+      {
+        type: 'string',
+        name: 'requiredParameter',
+        in: 'query',
+        required: true
+      },
+      {
+        type: 'string',
+        name: 'existParameter',
+        in: 'query',
+        required: true
+      },
+      {
+        type: 'string',
+        name: 'defaultParameter',
+        in: 'query'
+      },
+      {
+        type: 'string',
+        name: 'optionalParameter',
+        in: 'query',
+        required: false
+      }
+    ]);
+
+    expect(response.result.paths['/altParam'].get.parameters).to.equal([{
+      type: 'number',
+      'x-alternatives': [ { type: 'number' }, { type: 'string' } ],
+      name: 'altParam',
+      in: 'query'
+    }]);
+
+    const isValid = await Validate.test(response.result);
+    expect(isValid).to.be.true();
+  });
+
+  lab.test('parameter of object type', async () => {
+    let testRoutes = [{
+      method: 'GET',
+      path: '/emptyobject',
+      options: {
+        tags: ['api'],
+        handler: () => {},
+        validate: {
+          query: Joi.object({
+            objParam: Joi.object()
+          })
+        }
+      }
+    },{
+      method: 'GET',
+      path: '/objectWithProps',
+      options: {
+        tags: ['api'],
+        handler: () => {},
+        validate: {
+          query: Joi.object({
+            objParam: Joi.object({
+              stringParam: Joi.string()
+            })
+          })
+        }
+      }
+    },{
+      method: 'GET',
+      path: '/arrayOfObjects',
+      options: {
+        tags: ['api'],
+        handler: () => {},
+        validate: {
+          query: Joi.object({
+            arrayParam: Joi.array().items({
+              stringParam: Joi.string()
+            })
+          })
+        }
+      }
+    },{
+      method: 'GET',
+      path: '/arrayWithItemDescription',
+      options: {
+        tags: ['api'],
+        handler: () => {},
+        validate: {
+          query: Joi.object({
+            arrayParam: Joi.array().items(
+              Joi.object({
+                stringParam: Joi.string().description('String param description')
+              }).description('Item description')
+            ).description('Array description')
+          })
+        }
+      }
+    }];
+
+    const server = await Helper.createServer({}, testRoutes);
+    const response = await server.inject({ method: 'GET', url: '/swagger.json' });
+    expect(response.statusCode).to.equal(200);
+    expect(response.result.paths['/emptyobject'].get.parameters).to.equal([{
+      type: 'string',
+      name: 'objParam',
+      in: 'query',
+      'x-type': 'object'
+    }]);
+
+    expect(response.result.paths['/objectWithProps'].get.parameters).to.equal([{
+      type: 'string',
+      name: 'objParam',
+      in: 'query',
+      'x-type': 'object',
+      'x-properties': {
+        stringParam: {
+          type: 'string'
+        }
+      },
+    }]);
+
+    expect(response.result.paths['/arrayOfObjects'].get.parameters).to.equal([{
+      type: 'array',
+      name: 'arrayParam',
+      in: 'query',
+      items: {
+        type: 'string',
+        'x-type': 'object',
+        'x-properties': {
+          stringParam: {
+            type: 'string'
+          }
+        },
+      },
+      collectionFormat: 'multi',
+    }]);
+    expect(response.result.paths['/arrayWithItemDescription'].get.parameters).to.equal([{
+      type: 'array',
+      name: 'arrayParam',
+      in: 'query',
+      description: 'Array description',
+      items: {
+        type: 'string',
+        'x-type': 'object',
+        'x-properties': {
+          stringParam: {
+            type: 'string',
+            description: 'String param description'
+          }
+        },
+      },
+      collectionFormat: 'multi',
+    }]);
+
+    const isValid = await Validate.test(response.result, console.log);
+    expect(isValid).to.be.true();
+  });
+});
diff --git a/test/unit/property-test.js b/test/unit/property-test.js
index 35ca598..59e20bb 100644
--- a/test/unit/property-test.js
+++ b/test/unit/property-test.js
@@ -601,7 +601,6 @@ lab.test('parse type object', () => {
   expect(
     propertiesNoAlt.parseProperty('x', Joi.object().keys({ a: Joi.string() }), null, 'body', false, false)
   ).to.equal({
-    name: 'x',
     type: 'object',
     properties: {
       a: {
@@ -610,7 +609,6 @@ lab.test('parse type object', () => {
     }
   });
   expect(propertiesNoAlt.parseProperty('x', Joi.object({ a: Joi.string() }), null, 'body', false, false)).to.equal({
-    name: 'x',
     type: 'object',
     properties: {
       a: {
@@ -619,7 +617,6 @@ lab.test('parse type object', () => {
     }
   });
   expect(propertiesNoAlt.parseProperty('x', Joi.object({ a: Joi.string() }), null, 'body', false, false)).to.equal({
-    name: 'x',
     type: 'object',
     properties: {
       a: {
@@ -643,11 +640,9 @@ lab.test('parse type object', () => {
       false
     )
   ).to.equal({
-    name: 'x',
     type: 'object',
     properties: {
       string: {
-        name: 'string',
         type: 'object',
         properties: {
           y: {
@@ -673,11 +668,9 @@ lab.test('parse type object', () => {
       false
     )
   ).to.equal({
-    name: 'x',
     type: 'object',
     properties: {
       a: {
-        name: 'a',
         type: 'object',
         properties: {
           y: {
@@ -703,11 +696,9 @@ lab.test('parse type object', () => {
       false
     )
   ).to.equal({
-    name: 'x',
     type: 'object',
     properties: {
       string: {
-        name: 'string',
         type: 'object',
         properties: {
           y: {
@@ -751,7 +742,6 @@ lab.experiment('property deep - ', () => {
       type: 'object',
       properties: {
         outer1: {
-          name: 'outer1',
           type: 'object',
           properties: {
             inner1: {
@@ -764,7 +754,6 @@ lab.experiment('property deep - ', () => {
           required: ['inner1']
         },
         outer2: {
-          name: 'outer2',
           type: 'object',
           properties: {
             inner2: {