Skip to content

Commit d53621d

Browse files
fix: Deserialize custom types with inline schemas (#823)
1 parent f5bbce9 commit d53621d

File tree

3 files changed

+128
-7
lines changed

3 files changed

+128
-7
lines changed

Diff for: src/middlewares/parsers/schema.preprocessor.ts

+52-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface TraversalState {
2323

2424
interface TopLevelPathNodes {
2525
requestBodies: Root<SchemaObject>[];
26+
requestParameters: Root<SchemaObject>[];
2627
responses: Root<SchemaObject>[];
2728
}
2829
interface TopLevelSchemaNodes extends TopLevelPathNodes {
@@ -43,14 +44,31 @@ class Node<T, P> {
4344
}
4445
type SchemaObjectNode = Node<SchemaObject, SchemaObject>;
4546

47+
function isParameterObject(node: ParameterObject | ReferenceObject): node is ParameterObject {
48+
return !((node as ReferenceObject).$ref);
49+
}
50+
function isReferenceObject(node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject): node is ReferenceObject {
51+
return !!((node as ReferenceObject).$ref);
52+
}
53+
function isArraySchemaObject(node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject): node is ArraySchemaObject {
54+
return !!((node as ArraySchemaObject).items);
55+
}
56+
function isNonArraySchemaObject(node: ArraySchemaObject | NonArraySchemaObject | ReferenceObject): node is NonArraySchemaObject {
57+
return !isArraySchemaObject(node) && !isReferenceObject(node);
58+
}
59+
4660
class Root<T> extends Node<T, T> {
4761
constructor(schema: T, path: string[]) {
4862
super(null, schema, path);
4963
}
5064
}
5165

52-
type SchemaObject = OpenAPIV3.SchemaObject;
66+
type ArraySchemaObject = OpenAPIV3.ArraySchemaObject;
67+
type NonArraySchemaObject = OpenAPIV3.NonArraySchemaObject;
68+
type OperationObject = OpenAPIV3.OperationObject;
69+
type ParameterObject = OpenAPIV3.ParameterObject;
5370
type ReferenceObject = OpenAPIV3.ReferenceObject;
71+
type SchemaObject = OpenAPIV3.SchemaObject;
5472
type Schema = ReferenceObject | SchemaObject;
5573

5674
if (!Array.prototype['flatMap']) {
@@ -99,6 +117,7 @@ export class SchemaPreprocessor {
99117
schemas: componentSchemas,
100118
requestBodies: r.requestBodies,
101119
responses: r.responses,
120+
requestParameters: r.requestParameters,
102121
};
103122

104123
// Traverse the schemas
@@ -127,6 +146,7 @@ export class SchemaPreprocessor {
127146

128147
private gatherSchemaNodesFromPaths(): TopLevelPathNodes {
129148
const requestBodySchemas = [];
149+
const requestParameterSchemas = [];
130150
const responseSchemas = [];
131151

132152
for (const [p, pi] of Object.entries(this.apiDoc.paths)) {
@@ -140,14 +160,18 @@ export class SchemaPreprocessor {
140160
const node = new Root<OpenAPIV3.OperationObject>(operation, path);
141161
const requestBodies = this.extractRequestBodySchemaNodes(node);
142162
const responseBodies = this.extractResponseSchemaNodes(node);
163+
const requestParameters = this.extractRequestParameterSchemaNodes(node);
143164

144165
requestBodySchemas.push(...requestBodies);
145166
responseSchemas.push(...responseBodies);
167+
requestParameterSchemas.push(...requestParameters);
146168
}
147169
}
148170
}
171+
149172
return {
150173
requestBodies: requestBodySchemas,
174+
requestParameters: requestParameterSchemas,
151175
responses: responseSchemas,
152176
};
153177
}
@@ -230,6 +254,10 @@ export class SchemaPreprocessor {
230254
for (const node of nodes.responses) {
231255
recurse(null, node, initOpts());
232256
}
257+
258+
for (const node of nodes.requestParameters) {
259+
recurse(null, node, initOpts());
260+
}
233261
}
234262

235263
private schemaVisitor(
@@ -505,6 +533,28 @@ export class SchemaPreprocessor {
505533
return schemas;
506534
}
507535

536+
private extractRequestParameterSchemaNodes(
537+
operationNode: Root<OperationObject>,
538+
): Root<SchemaObject>[] {
539+
540+
return (operationNode.schema.parameters ?? []).flatMap((node) => {
541+
const parameterObject = isParameterObject(node) ? node : undefined;
542+
if (!parameterObject?.schema) return [];
543+
544+
const schema = isNonArraySchemaObject(parameterObject.schema) ?
545+
parameterObject.schema :
546+
undefined;
547+
if (!schema) return [];
548+
549+
return new Root(schema, [
550+
...operationNode.path,
551+
'parameters',
552+
parameterObject.name,
553+
parameterObject.in
554+
]);
555+
});
556+
}
557+
508558
private resolveSchema<T>(schema): T {
509559
if (!schema) return null;
510560
const ref = schema?.['$ref'];
@@ -541,7 +591,7 @@ export class SchemaPreprocessor {
541591
) =>
542592
// if name or ref exists and are equal
543593
(opParam['name'] && opParam['name'] === pathParam['name']) ||
544-
(opParam['$ref'] && opParam['$ref'] === pathParam['$ref']);
594+
(opParam['$ref'] && opParam['$ref'] === pathParam['$ref']);
545595

546596
// Add Path level query param to list ONLY if there is not already an operation-level query param by the same name.
547597
for (const param of parameters) {

Diff for: test/resources/serdes.yaml

+44-3
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,17 @@ paths:
1313
required: true
1414
schema:
1515
$ref: "#/components/schemas/ObjectId"
16+
- name: date-time-from-inline
17+
in: query
18+
required: false
19+
schema:
20+
type: string
21+
format: date-time
22+
- name: date-time-from-schema
23+
in: query
24+
required: false
25+
schema:
26+
$ref: "#/components/schemas/DateTime"
1627
- name: baddateresponse
1728
in: query
1829
schema:
@@ -29,21 +40,51 @@ paths:
2940
content:
3041
application/json:
3142
schema:
32-
$ref: "#/components/schemas/User"
43+
allOf:
44+
- $ref: "#/components/schemas/User"
45+
- type: object
46+
properties:
47+
summary:
48+
type: object
49+
additionalProperties:
50+
type: object
51+
properties:
52+
value:
53+
type: string
54+
typeof:
55+
type: string
3356
/users:
3457
post:
3558
requestBody:
3659
content :
3760
application/json:
3861
schema:
39-
$ref: '#/components/schemas/User'
62+
allOf:
63+
- $ref: '#/components/schemas/User'
64+
- type: object
65+
properties:
66+
creationDateTimeInline:
67+
type: string
68+
format: date-time
4069
responses:
4170
200:
4271
description: ""
4372
content:
4473
application/json:
4574
schema:
46-
$ref: "#/components/schemas/User"
75+
allOf:
76+
- $ref: "#/components/schemas/User"
77+
- type: object
78+
properties:
79+
summary:
80+
type: object
81+
additionalProperties:
82+
type: object
83+
properties:
84+
value:
85+
type: string
86+
typeof:
87+
type: string
4788
components:
4889
schemas:
4990
ObjectId:

Diff for: test/serdes.spec.ts

+32-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ class BadDate extends Date {
2626
}
2727
}
2828

29+
function toSummary(title, value) {
30+
return {
31+
[title]: {
32+
value: value?.toISOString?.() || value?.toString(),
33+
typeof: typeof value
34+
}
35+
}
36+
}
37+
2938
describe('serdes', () => {
3039
let app = null;
3140

@@ -63,6 +72,10 @@ describe('serdes', () => {
6372
creationDateTime: date,
6473
creationDate: date,
6574
shortOrLong: 'a',
75+
summary: {
76+
...toSummary('req.query.date-time-from-inline', req.query['date-time-from-inline']),
77+
...toSummary('req.query.date-time-from-schema', req.query['date-time-from-schema']),
78+
},
6679
});
6780
});
6881
app.post([`${app.basePath}/users`], (req, res) => {
@@ -75,7 +88,13 @@ describe('serdes', () => {
7588
if (typeof req.body.creationDateTime !== 'object' || !(req.body.creationDateTime instanceof Date)) {
7689
throw new Error("Should be deserialized to Date object");
7790
}
78-
res.json(req.body);
91+
if (typeof req.body.creationDateTimeInline !== 'object' || !(req.body.creationDateTimeInline instanceof Date)) {
92+
throw new Error("Should be deserialized to Date object");
93+
}
94+
res.json({
95+
...req.body,
96+
summary: Object.entries(req.body).reduce((acc, [k, v]) => Object.assign(acc, toSummary(k, v)), {})
97+
});
7998
});
8099
app.use((err, req, res, next) => {
81100
res.status(err.status ?? 500).json({
@@ -103,12 +122,16 @@ describe('serdes', () => {
103122

104123
it('should control GOOD id format and get a response in expected format', async () =>
105124
request(app)
106-
.get(`${app.basePath}/users/5fdefd13a6640bb5fb5fa925`)
125+
.get(`${app.basePath}/users/5fdefd13a6640bb5fb5fa925?date-time-from-inline=2019-11-20T01%3A11%3A54.930Z&date-time-from-schema=2020-11-20T01%3A11%3A54.930Z`)
107126
.expect(200)
108127
.then((r) => {
109128
expect(r.body.id).to.equal('5fdefd13a6640bb5fb5fa925');
110129
expect(r.body.creationDate).to.equal('2020-12-20');
111130
expect(r.body.creationDateTime).to.equal("2020-12-20T07:28:19.213Z");
131+
expect(r.body.summary['req.query.date-time-from-schema'].value).to.equal("2020-11-20T01:11:54.930Z");
132+
expect(r.body.summary['req.query.date-time-from-schema'].typeof).to.equal("object");
133+
expect(r.body.summary['req.query.date-time-from-inline'].value).to.equal("2019-11-20T01:11:54.930Z");
134+
expect(r.body.summary['req.query.date-time-from-inline'].typeof).to.equal("object");
112135
}));
113136

114137
it('should POST also works with deserialize on request then serialize en response', async () =>
@@ -117,6 +140,7 @@ describe('serdes', () => {
117140
.send({
118141
id: '5fdefd13a6640bb5fb5fa925',
119142
creationDateTime: '2020-12-20T07:28:19.213Z',
143+
creationDateTimeInline: '2019-11-21T07:24:19.213Z',
120144
creationDate: '2020-12-20',
121145
shortOrLong: 'ab',
122146
})
@@ -126,6 +150,12 @@ describe('serdes', () => {
126150
expect(r.body.id).to.equal('5fdefd13a6640bb5fb5fa925');
127151
expect(r.body.creationDate).to.equal('2020-12-20');
128152
expect(r.body.creationDateTime).to.equal("2020-12-20T07:28:19.213Z");
153+
expect(r.body.summary['creationDate'].value).to.equal('2020-12-20T00:00:00.000Z');
154+
expect(r.body.summary['creationDate'].typeof).to.equal('object');
155+
expect(r.body.summary['creationDateTime'].value).to.equal('2020-12-20T07:28:19.213Z');
156+
expect(r.body.summary['creationDateTime'].typeof).to.equal('object');
157+
expect(r.body.summary['creationDateTimeInline'].value).to.equal('2019-11-21T07:24:19.213Z');
158+
expect(r.body.summary['creationDateTimeInline'].typeof).to.equal('object');
129159
}));
130160

131161
it('should POST throw error on invalid schema ObjectId', async () =>

0 commit comments

Comments
 (0)