Skip to content

Commit a8a48eb

Browse files
authored
Merge pull request #1562 from iffa/fix/openapi-3-enum-nullable
Fix invalid implementation of nullable enum
2 parents bdf90fc + f6a1f47 commit a8a48eb

File tree

5 files changed

+55
-15
lines changed

5 files changed

+55
-15
lines changed

packages/cli/src/swagger/specGenerator3.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { merge as mergeAnything } from 'merge-anything'
1+
import { Swagger, Tsoa, assertNever } from '@tsoa/runtime';
2+
import { merge as mergeAnything } from 'merge-anything';
23
import { merge as deepMerge } from 'ts-deepmerge';
3-
import { Tsoa, assertNever, Swagger } from '@tsoa/runtime';
44

55
import { ExtendedSpecConfig } from '../cli';
66
import { isVoidType } from '../utils/isVoidType';
7+
import { UnspecifiedObject } from '../utils/unspecifiedObject';
8+
import { shouldIncludeValidatorInSchema } from '../utils/validatorUtils';
79
import { convertColonPathParams, normalisePath } from './../utils/pathUtils';
810
import { DEFAULT_REQUEST_MEDIA_TYPE, DEFAULT_RESPONSE_MEDIA_TYPE, getValue } from './../utils/swaggerUtils';
911
import { SpecGenerator } from './specGenerator';
10-
import { UnspecifiedObject } from '../utils/unspecifiedObject';
11-
import { shouldIncludeValidatorInSchema } from '../utils/validatorUtils';
1212

1313
/**
1414
* TODO:
@@ -20,7 +20,10 @@ import { shouldIncludeValidatorInSchema } from '../utils/validatorUtils';
2020
* Also accept OpenAPI 3.0.0 metadata, like components/securitySchemes instead of securityDefinitions
2121
*/
2222
export class SpecGenerator3 extends SpecGenerator {
23-
constructor(protected readonly metadata: Tsoa.Metadata, protected readonly config: ExtendedSpecConfig) {
23+
constructor(
24+
protected readonly metadata: Tsoa.Metadata,
25+
protected readonly config: ExtendedSpecConfig,
26+
) {
2427
super(metadata, config);
2528
}
2629

@@ -660,6 +663,13 @@ export class SpecGenerator3 extends SpecGenerator {
660663
if (swaggerType.$ref) {
661664
return { allOf: [swaggerType], nullable };
662665
}
666+
667+
// Note that null must be explicitly included in the list of enum values. Using nullable: true alone is not enough here.
668+
// https://swagger.io/docs/specification/data-models/enums/
669+
if (swaggerType.enum) {
670+
swaggerType.enum.push(null);
671+
}
672+
663673
return { ...(title && { title }), ...swaggerType, nullable };
664674
} else {
665675
return { ...(title && { title }), anyOf: actualSwaggerTypes, nullable };

tests/fixtures/controllers/getController.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
///<reference path="../tsoaTestModule.d.ts" />
2+
import { Controller, Example, Get, OperationId, Queries, Query, Request, Res, Response, Route, SuccessResponse, Tags, TsoaResponse } from '@tsoa/runtime';
23
import { Readable } from 'stream';
3-
import { Controller, Example, Get, OperationId, Query, Request, Response, Route, SuccessResponse, Tags, Res, TsoaResponse, Queries } from '@tsoa/runtime';
4+
import TsoaTest from 'tsoaTest';
45
import '../duplicateTestModel';
56
import {
67
GenericModel,
78
GetterClass,
89
GetterInterface,
910
GetterInterfaceHerited,
10-
TestClassModel,
11-
TestModel,
12-
TestSubModel,
13-
SimpleClassWithToJSON,
1411
IndexedValue,
15-
IndexedValueReference,
16-
ParenthesizedIndexedValue,
1712
IndexedValueGeneric,
13+
IndexedValueReference,
1814
IndexedValueTypeReference,
15+
ParenthesizedIndexedValue,
16+
SimpleClassWithToJSON,
17+
TestClassModel,
18+
TestModel,
19+
TestSubModel,
1920
} from '../testModel';
2021
import { ModelService } from './../services/modelService';
21-
import TsoaTest from 'tsoaTest';
2222

2323
export const PathFromConstant = 'PathFromConstantValue';
2424
export enum EnumPaths {
@@ -66,6 +66,7 @@ export class GetTestController extends Controller {
6666
strLiteralVal: 'Foo',
6767
stringArray: ['string one', 'string two'],
6868
stringValue: 'a string',
69+
nullableStringLiteral: 'NULLABLE_LIT_1',
6970
undefineableUnionPrimitiveType: undefined,
7071
undefinedValue: undefined,
7172
})

tests/fixtures/testModel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface TestModel extends Model {
6767
modelsArray: TestSubModel[];
6868
strLiteralVal: StrLiteral;
6969
strLiteralArr: StrLiteral[];
70+
nullableStringLiteral?: 'NULLABLE_LIT_1' | 'NULLABLE_LIT_2' | null;
7071
unionPrimitiveType?: 'String' | 1 | 20.0 | true | false;
7172
nullableUnionPrimitiveType?: 'String' | 1 | 20.0 | true | false | null;
7273
undefineableUnionPrimitiveType: 'String' | 1 | 20.0 | true | false | undefined;

tests/unit/swagger/definitionsGeneration/definitions.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { SpecGenerator2 } from '@tsoa/cli/swagger/specGenerator2';
44
import { Swagger } from '@tsoa/runtime';
55
import { expect } from 'chai';
66
import 'mocha';
7+
import * as os from 'os';
78
import { versionMajorMinor } from 'typescript';
89
import { getDefaultOptions } from '../../../fixtures/defaultOptions';
910
import { TestModel } from '../../../fixtures/testModel';
10-
import * as os from 'os';
1111

1212
describe('Definition generation', () => {
1313
const metadata = new MetadataGenerator('./fixtures/controllers/getController.ts').Generate();
@@ -2853,6 +2853,20 @@ describe('Definition generation', () => {
28532853
const mappedTypedConditionalSchema = getValidatedDefinition('Partial_Conditional_string.string._a-number_.never__', currentSpec);
28542854
expect(mappedTypedConditionalSchema).to.deep.eq(mappedConditionalSchema, `for property ${propertyName}.mappedTypedConditional`);
28552855
},
2856+
nullableStringLiteral: (propertyName, propertySchema) => {
2857+
expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`);
2858+
expect(propertySchema['x-nullable']).to.eq(true, `for property ${propertyName}[x-nullable]`);
2859+
2860+
expect(propertySchema).to.deep.eq({
2861+
type: 'string',
2862+
enum: ['NULLABLE_LIT_1', 'NULLABLE_LIT_2', null],
2863+
['x-nullable']: true,
2864+
description: undefined,
2865+
example: undefined,
2866+
format: undefined,
2867+
default: undefined,
2868+
});
2869+
},
28562870
typeOperators: (propertyName, propertySchema) => {
28572871
expect(propertySchema?.properties?.keysOfAny?.$ref).to.eq('#/definitions/KeysMember', `for property ${propertyName}`);
28582872
expect(propertySchema?.properties?.keysOfInterface?.$ref).to.eq('#/definitions/KeysMember_NestedTypeLiteral_', `for property ${propertyName}`);

tests/unit/swagger/schemaDetails3.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { SpecGenerator3 } from '@tsoa/cli/swagger/specGenerator3';
44
import { Swagger, Tsoa } from '@tsoa/runtime';
55
import { expect } from 'chai';
66
import 'mocha';
7+
import * as os from 'os';
78
import { versionMajorMinor } from 'typescript';
89
import { getDefaultExtendedOptions } from '../../fixtures/defaultOptions';
910
import { TestModel } from '../../fixtures/testModel';
10-
import * as os from 'os';
1111

1212
describe('Definition generation for OpenAPI 3.0.0', () => {
1313
const metadataGet = new MetadataGenerator('./fixtures/controllers/getController.ts').Generate();
@@ -4425,6 +4425,20 @@ describe('Definition generation for OpenAPI 3.0.0', () => {
44254425
`for property ${propertyName}.separateField3.omitted`,
44264426
);
44274427
},
4428+
nullableStringLiteral: (propertyName, propertySchema) => {
4429+
expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`);
4430+
expect(propertySchema.nullable).to.eq(true, `for property ${propertyName}[x-nullable]`);
4431+
4432+
expect(propertySchema).to.deep.eq({
4433+
type: 'string',
4434+
enum: ['NULLABLE_LIT_1', 'NULLABLE_LIT_2', null],
4435+
nullable: true,
4436+
description: undefined,
4437+
example: undefined,
4438+
format: undefined,
4439+
default: undefined,
4440+
});
4441+
},
44284442
};
44294443

44304444
const testModel = currentSpec.spec.components.schemas[interfaceModelName];

0 commit comments

Comments
 (0)