Skip to content

Deep objects are not fully escaped with deepObject and explode #2659

Open
@bakkerthehacker

Description

When using parameters with deepObject and explode, the parameters are only partially escaped.

Q&A (please complete the following information)

  • OS: linux mint 20.3
  • Environment: node v14.19.2
  • Method of installation: npm install
  • Swagger-Client version: 3.18.5
  • Swagger/OpenAPI version: OpenAPI 3.0.3

Content & configuration

Swagger/OpenAPI definition:

const spec = {
  openapi: '3.0.3',
  paths: {
    '/export': {
      post: {
        operationId: 'exportProducts',
        parameters: [
          {
            name: 'filters',
            in: 'query',
            style: 'deepObject',
            explode: true,
            schema: { type: 'object', additionalProperties: true },
          },
          {
            name: 'filtersDeep',
            in: 'query',
            style: 'deepObject',
            explode: true,
            schema: { type: 'object', additionalProperties: true },
          },
        ],
      },
    },
  },
};

Swagger-Client usage:

const parameters = {
  filters: { 'a+b+c': '123=456' },
  filtersDeep: { 'a+b+c': { 'd+e+f': '123=456' } },
};

const SwaggerClient = require('swagger-client');

const request = SwaggerClient.buildRequest({ spec, operationId: 'exportProducts', parameters });

console.log(request.url);
// prints:
// /export?filters%5Ba%2Bb%2Bc%5D=123%3D456&filtersDeep%5Ba%2Bb%2Bc%5D[d+e+f]=123=456

Describe the bug you're encountering

When using parameters with deepObject and explode, the parameters are only partially escaped. If an object with only 1 depth is provided, it is escaped correctly. But objects with 2 or more depth levels are not escaped correctly. In deeper objects, only the parameter name and 1st level of keys are escaped. Nothing else is escaped including the value.

To reproduce...

Steps to reproduce the behavior:

  1. Create spec with deepObject and explode parameters.
  2. Pass in a 1 depth object as a parameter, observe it is fully escaped.
  3. Pass in a 2 depth object as a parameter, observe it is not fully escaped.

Expected behavior

I expect deep objects passed as parameters with deepObject and explode to be fully escaped, including all the keys and values.

Additional context or thoughts

I see that there are other tickets/issues/prs for deepObject/explode support so I'm not sure how finalized these parameters are. However, the behaviour here doesn't seem correct in any situation.

I have a hacky solution to apply a fix through the parameterBuilders system. It is a bit hacky, both because it grabs the default un-exported oas3 builder and it modifies the value using qs before the http escaping system happens. Changes to serializationOption had no effect.

Click for fix with parameterBuilders
// npm install swagger-client@3.18.5
// npm install qs@6.11.0

const parameters = {
  filters: { 'a+b+c': '123=456' },
  filtersDeep: { 'a+b+c': { 'd+e+f': '123=456' } },
};

// QS version:
const qs = require('qs');

console.log('QS Version:');
console.log(`/export?${qs.stringify(parameters)}`);

// Swagger version:
const SwaggerClient = require('swagger-client');

const spec = {
  openapi: '3.0.3',
  paths: {
    '/export': {
      post: {
        operationId: 'exportProducts',
        parameters: [
          {
            name: 'filters',
            in: 'query',
            style: 'deepObject',
            explode: true,
            schema: { type: 'object', additionalProperties: true },
          },
          {
            name: 'filtersDeep',
            in: 'query',
            style: 'deepObject',
            explode: true,
            schema: { type: 'object', additionalProperties: true },
          },
        ],
      },
    },
  },
};

const request = SwaggerClient.buildRequest({ spec, operationId: 'exportProducts', parameters });

console.log('Swagger Version:');
console.log(request.url);

// Customize with parameterBuilders
const oas3Builder = require('swagger-client/lib/execute/oas3/parameter-builders');

const requestCustomBuilder = SwaggerClient.buildRequest({
  spec,
  operationId: 'exportProducts',
  parameters,
  parameterBuilders: {
    body: oas3Builder.body,
    header: oas3Builder.header,
    path: oas3Builder.path,
    formData: oas3Builder.formData,
    cookie: oas3Builder.cookie,
    query: ({ req, value, parameter }) => {
      if (parameter.style === 'deepObject' && parameter.explode) {
        const flatParams = [...new URLSearchParams(qs.stringify({ value }))];
        const newValue = Object.fromEntries(flatParams.map(
          ([key, val]) => [key.replace(/^value\[/, '').replace(/]$/, ''), val],
        ));
        oas3Builder.query({ req, value: newValue, parameter });
      } else {
        oas3Builder.query({ req, value, parameter });
      }
    },
  },
});

console.log('Swagger Custom Builder Version:');
console.log(requestCustomBuilder.url);

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions