Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/runtime/src/routeGeneration/templateHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,19 @@ export class ValidationService {
);
}

// When value is null or undefined, prefer explicit null/undefined subschemas over
// coercive type matches. Without this, e.g. `boolean | null` with value null would
// coerce null to false (via validateBool) before ever reaching the null type.
if (value === null || value === undefined) {
for (const subSchema of property.subSchemas) {
const isExactNull = value === null && subSchema.dataType === 'enum' && subSchema.enums?.includes(null);
const isExactUndefined = value === undefined && subSchema.dataType === 'undefined';
if (isExactNull || isExactUndefined) {
return value;
}
}
}

const subFieldErrors: FieldErrors[] = [];

for (const subSchema of property.subSchemas) {
Expand Down
58 changes: 58 additions & 0 deletions tests/unit/templating/templateHelpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,64 @@ it('should throw if the data has additionalProperties (on a union) if noImplicit
}
});

it('should return null (not false) when validating null against a union of `boolean | null` (bodyCoercion enabled)', () => {
const unionProperty: TsoaRoute.PropertySchema = {
dataType: 'union',
subSchemas: [{ dataType: 'boolean' }, { dataType: 'enum', enums: [null] }],
};
const v = new ValidationService({}, { noImplicitAdditionalProperties: 'ignore', bodyCoercion: true });
const errors: FieldErrors = {};

// Act
const result = v.validateUnion('field', null, errors, true, unionProperty, '');

// Assert
expect(errors).to.deep.eq({});
expect(result).to.equal(null);
});

it('should return null when validating null against a union of `null | boolean` (order reversed, regression guard)', () => {
const unionProperty: TsoaRoute.PropertySchema = {
dataType: 'union',
subSchemas: [{ dataType: 'enum', enums: [null] }, { dataType: 'boolean' }],
};
const v = new ValidationService({}, { noImplicitAdditionalProperties: 'ignore', bodyCoercion: true });
const errors: FieldErrors = {};

const result = v.validateUnion('field', null, errors, true, unionProperty, '');

expect(errors).to.deep.eq({});
expect(result).to.equal(null);
});

it('should return true when validating true against a union of `boolean | null`', () => {
const unionProperty: TsoaRoute.PropertySchema = {
dataType: 'union',
subSchemas: [{ dataType: 'boolean' }, { dataType: 'enum', enums: [null] }],
};
const v = new ValidationService({}, { noImplicitAdditionalProperties: 'ignore', bodyCoercion: true });
const errors: FieldErrors = {};

const result = v.validateUnion('field', true, errors, true, unionProperty, '');

expect(errors).to.deep.eq({});
expect(result).to.equal(true);
});

it('should return false when validating false against a union of `boolean | null`', () => {
const unionProperty: TsoaRoute.PropertySchema = {
dataType: 'union',
subSchemas: [{ dataType: 'boolean' }, { dataType: 'enum', enums: [null] }],
};
const v = new ValidationService({}, { noImplicitAdditionalProperties: 'ignore', bodyCoercion: true });
const errors: FieldErrors = {};

const result = v.validateUnion('field', false, errors, true, unionProperty, '');

expect(errors).to.deep.eq({});
expect(result).to.equal(false);
});

it('should throw if the data has additionalProperties (on a intersection) if noImplicitAdditionalProperties is set to throw-on-extras', () => {
// Arrange
const refName = 'ExampleModel';
Expand Down
Loading