Skip to content

Commit c0fe651

Browse files
committed
fix: suport complex objects for query params in api explorer
BREAKING CHANGE: This fix has modified the schema described by the decorator 'param.query.object', to support Open-API specification's url-encoded format for json query parameters. Previously, such parameters were described with `exploded: true`, `style: deepObject` which turned out to be problematic, as explained and discussed in, swagger-api/swagger-js#1385 and OAI/OpenAPI-Specification#1706 ```json { "in": "query", "style": "deepObject" "explode": "true", "schema": {} } ``` Exploded encoding worked for json query params if the payload was simple as below, but not for complex objects. ``` http://localhost:3000/todos?filter[limit]=2 ``` To address these issues with exploded queries, this fix modifies the definition of JSON query params from `exploded` and `deep-object` style to the `url-encoded` style described in Open-API spec. The 'style' and 'explode' fields are removed. The 'schema' field is moved under 'content[application/json]' (`url-encoded` style) as below, ```json { "in": "query" "content": { "application/json": { "schema": {} } } } ``` for example, to filter api results with the following condition, ```json { "include": [ { "relation": "todo" } ] } ``` the following url-encoded query parameter is used. ``` http://localhost:3000/todos?filter=%7B%22include%22%3A%5B%7B%22relation%22%3A%22todoList%22%7D%5D%7D ``` To preserve compatibility with existing REST API clients, this change is backward compatible with all previously supported formats for json query parameters. Exploded queries like `?filter[limit]=1` will continue to work, despite the fact that they are described differently in the OpenAPI spec. As a result, existing REST API clients will keep working after an upgrade. LoopBack supported receiving `url-encoded` payload for `exploded`, `deep object` style query params on the server side even before this fix, even though the Open-API spec has very clear demarcations for both these styles. In effect, this fix only clarifies the schema contract as per Open-API spec. The impact is only on the open api definitions generated from LoopBack APIs. The signature of the 'param.query.object' decorator has not changed. There is no code changes required in the LoopBack APIs after upgrading to this fix. No method signatures or data structures are impacted. All consumers of LoopBack APIs may need to regenerate api definitions, if their API clients or client tools (like swagger-ui or LoopBack's api explorer) necessiate using Open-API's `url-encoded` format to support url-encoding. Otherwise there wouldn't be any significant impact on API consumers.
1 parent 56543fe commit c0fe651

File tree

3 files changed

+36
-24
lines changed

3 files changed

+36
-24
lines changed

packages/openapi-v3/src/__tests__/unit/decorators/param/param-query.decorator.unit.ts

+17-17
Original file line numberDiff line numberDiff line change
@@ -229,11 +229,10 @@ describe('Routing metadata for parameters', () => {
229229
const expectedParamSpec = <ParameterObject>{
230230
name: 'filter',
231231
in: 'query',
232-
style: 'deepObject',
233-
explode: true,
234-
schema: {
235-
type: 'object',
236-
additionalProperties: true,
232+
content: {
233+
'application/json': {
234+
schema: {type: 'object', additionalProperties: true},
235+
},
237236
},
238237
};
239238
expectSpecToBeEqual(MyController, expectedParamSpec);
@@ -256,13 +255,15 @@ describe('Routing metadata for parameters', () => {
256255
const expectedParamSpec: ParameterObject = {
257256
name: 'filter',
258257
in: 'query',
259-
style: 'deepObject',
260-
explode: true,
261-
schema: {
262-
type: 'object',
263-
properties: {
264-
where: {type: 'object', additionalProperties: true},
265-
limit: {type: 'number'},
258+
content: {
259+
'application/json': {
260+
schema: {
261+
type: 'object',
262+
properties: {
263+
where: {type: 'object', additionalProperties: true},
264+
limit: {type: 'number'},
265+
},
266+
},
266267
},
267268
},
268269
};
@@ -300,11 +301,10 @@ describe('Routing metadata for parameters', () => {
300301
name: 'filter',
301302
in: 'query',
302303
description: 'Search criteria',
303-
style: 'deepObject',
304-
explode: true,
305-
schema: {
306-
type: 'object',
307-
additionalProperties: true,
304+
content: {
305+
'application/json': {
306+
schema: {type: 'object', additionalProperties: true},
307+
},
308308
},
309309
};
310310
expectSpecToBeEqual(MyController, expectedParamSpec);

packages/openapi-v3/src/decorators/parameter.decorator.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,11 @@ export function param(paramSpec: ParameterObject) {
5050
// generate schema if `paramSpec` has `schema` but without `type`
5151
(isSchemaObject(paramSpec.schema) && !paramSpec.schema.type)
5252
) {
53-
// please note `resolveSchema` only adds `type` and `format` for `schema`
54-
paramSpec.schema = resolveSchema(paramType, paramSpec.schema);
53+
// If content explicitly mentioned do not resolve schema
54+
if (!paramSpec.content) {
55+
// please note `resolveSchema` only adds `type` and `format` for `schema`
56+
paramSpec.schema = resolveSchema(paramType, paramSpec.schema);
57+
}
5558
}
5659
}
5760

@@ -212,9 +215,11 @@ export namespace param {
212215
return param({
213216
name,
214217
in: 'query',
215-
style: 'deepObject',
216-
explode: true,
217-
schema,
218+
content: {
219+
'application/json': {
220+
schema,
221+
},
222+
},
218223
...spec,
219224
});
220225
},

packages/rest/src/coercion/coerce-parameter.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ export function coerceParameter(
3232
data: string | undefined | object,
3333
spec: ParameterObject,
3434
) {
35-
const schema = spec.schema;
35+
let schema = spec.schema;
36+
37+
if (!schema && spec.content) {
38+
const content = spec.content;
39+
const jsonSpec = content['application/json'];
40+
schema = jsonSpec.schema;
41+
}
42+
3643
if (!schema || isReferenceObject(schema)) {
3744
debug(
3845
'The parameter with schema %s is not coerced since schema' +
@@ -172,7 +179,7 @@ function parseJsonIfNeeded(
172179
): string | object | undefined {
173180
if (typeof data !== 'string') return data;
174181

175-
if (spec.in !== 'query' || spec.style !== 'deepObject') {
182+
if (spec.in !== 'query' || (spec.style !== 'deepObject' && !spec.content)) {
176183
debug(
177184
'Skipping JSON.parse, argument %s is not in:query style:deepObject',
178185
spec.name,

0 commit comments

Comments
 (0)