Description
Description
I'm trying to implement different validations across inherited classes.
Right now, the behavior is either failing or not working as expected.
Specifically, I'm running into issues with my DTOs in NestJS, especially when using PartialType and similar utilities.
The Idea
- UpdateDTO extends CreateDTO
- CreateDTO extends BaseDTO
Each level adds or overrides validation rules.
In my opinion, all tests should pass, but I'm seeing some weird behavior.
Let me start by introducing the code…
Minimal code-snippet showcasing the problem
import {
IsAlpha,
IsAlphanumeric,
IsNotEmpty,
IsString,
IsUUID,
validate,
} from 'class-validator';
export class Base {
@IsNotEmpty()
@IsUUID('4')
baseField: string;
}
export class Create extends Base {
@IsString()
baseField: string;
@IsAlpha()
createField: string;
}
describe('Create', () => {
let create: Create;
describe('Checking Constraints', () => {
beforeEach(() => {
create = new Create();
});
// FAIL. It fails because isUuid and IsNotEmpty doesn't triggered.
// Q1: What is the expected behavior here?
test('baseField', async () => {
const results = await validate(create);
const baseFieldResult = results.find(
(result) => result.property === 'baseField',
);
expect(baseFieldResult?.constraints).toMatchObject({
isUuid: expect.anything(), // from Base, it is missing
isNotEmpty: expect.anything(), // from Base, it is missing
isAlpha: expect.anything(), // from Create
});
});
// PASS
test('createField', async () => {
const results = await validate(create);
const createFieldResult = results.find(
(result) => result.property === 'createField',
);
expect(createFieldResult?.constraints).toMatchObject({
isAlpha: expect.anything(),
});
});
});
describe('Checking with different values', () => {
beforeEach(() => {
create = new Create();
});
// PASS.
test('baseField should be valid', async () => {
create.baseField = '91a65d06-995c-401f-a51e-056b4bbdf101';
create.createField = 'asdf';
const results = await validate(create);
expect(results).toHaveLength(0);
});
// FAIL. isString is the unique validation triggered.
// Q1.1. I think its the same question. should it fail with all the validations? or just the current one?
test('baseField should be invalid', async () => {
create.createField = 'asdf';
const results = await validate(create);
expect(results).toHaveLength(1);
expect(results).toEqual(
expect.arrayContaining([
expect.objectContaining({
constraints: expect.objectContaining({
isUuid: expect.anything(), // from Base (FAIL)
isNotEmpty: expect.anything(), // from Base (FAIL)
isString: expect.anything(), // from Update (OK)
}),
}),
]),
);
});
// FAIL. This is a big fail. It should trigger the isUuid validation, but it pass.
// Q2. Validations should be chained? or we are going to add the decorators again?
test('baseField should be invalid by uuid format', async () => {
create.baseField = 'asdf'; // it should be an uuid
create.createField = 'asdf';
const results = await validate(create);
expect(results).toHaveLength(1); // FAILS HERE! WHY?
expect(results).toEqual(
expect.arrayContaining([
expect.objectContaining({
constraints: expect.objectContaining({
isUuid: expect.anything(), // from Base (FAIL)
}),
}),
]),
);
});
});
});
// Update extends Create
export class Update extends Create {
baseField: string;
@IsUUID('4')
createField: string;
}
describe('Update', () => {
let update: Update;
describe('Checking Constraints', () => {
beforeEach(() => {
update = new Update();
});
// PASS
test('baseField', async () => {
const results = await validate(update);
const baseFieldResult = results.find(
(result) => result.property === 'baseField',
);
expect(baseFieldResult?.constraints).toMatchObject({
isUuid: expect.anything(), // from Base
isNotEmpty: expect.anything(), // from Base
isString: expect.anything(), // from Create
});
});
// FAIL.
// Q1.2. Same as last examples, should it fail all the constrains?
test('createField', async () => {
const results = await validate(update);
const createFieldResult = results.find(
(result) => result.property === 'createField',
);
expect(createFieldResult?.constraints).toMatchObject({
isAlpha: expect.anything(), // from Create, it is missing (FAIL)
isUuid: expect.anything(), // from Update (OK)
});
});
});
describe('Checking with different values', () => {
beforeEach(() => {
update = new Update();
});
// FAIL.
// Q2.1. Same. Should the validation pass through the inheritance chain?
test('baseField should be valid, createField should be invalid by uuid format', async () => {
update.baseField = '91a65d06-995c-401f-a51e-056b4bbdf101';
update.createField = 'asdf';
const results = await validate(update);
console.log(results);
expect(results).toHaveLength(1);
expect(results).toEqual(
expect.arrayContaining([
expect.objectContaining({
constraints: expect.objectContaining({
isAlpha: expect.anything(), // from Create (FAIL)
isUuid: expect.anything(), // from Update (OK)
}),
}),
]),
);
});
// FAIL. Same. Isn't validate from the inheritance (its overriden)
// Q2.2. Same. It looks like the validation are being overriden instead of chained
test('baseField should be valid', async () => {
update.baseField = '91a65d06-995c-401f-a51e-056b4bbdf101';
update.createField = '91a65d06-995c-401f-a51e-056b4bbdf101';
const results = await validate(update);
expect(results).toHaveLength(1); // FAIL HERE!
expect(results).toEqual(
expect.arrayContaining([
expect.objectContaining({
constraints: expect.objectContaining({
isAlpha: expect.anything(), // from Create (FAIL)
}),
}),
]),
);
});
});
});
The problem:
We have a pattern that it was working and now is not working.
It's a similar pattern using PartialType, and essentially, it's the same.
The pattern is the following:
- BaseDto: some special shared fields
- CreateDto: all the fields, it inherits from BaseDto
- UpdateDto: some overriding fields (for example to made them required) and all the others optional (with
PartialType(CreateDto)
)
To clarify:
Q1, Q2, Q3: How does the behavior work during overriding?
Is there a way to receive all validation errors in the same invocation?
Imagine you're working on a frontend.
You need to fill in a field that has a @IsNotEmpty
validation. It fails.
Then you fill it in, but now it fails again—this time due to another validation (e.g., @IsUUID
).
And so on…
In spite of that, with these examples it looks like the overriding and chain validation fields are not longer available.
The other example with PartialType.
import { PartialType } from '@nestjs/mapped-types';
import { IsNotEmpty, IsString, IsUUID, validate } from 'class-validator';
export class Base {
@IsString()
@IsUUID('4')
url: string;
}
export class Create extends Base {
@IsNotEmpty()
url: string;
}
// Update extends Create
export class UpdatePartial extends PartialType(Create) {}
describe('Create', () => {
let create: Create;
beforeEach(() => {
create = new Create();
});
// FAIL
it('should fail if url is not uuid', async () => {
create.url = 'asdf';
const results = await validate(create);
expect(results.length).toBe(1); // FAIL. Not detecting IsUUID.
});
// PASS
it('should pass if url is uuid', async () => {
create.url = '91a65d06-995c-401f-a51e-056b4bbdf101';
const results = await validate(create);
expect(results.length).toBe(0);
});
// PASS.
it('should fail if create is empty', async () => {
const results = await validate(create);
expect(results.length).toBe(1);
});
});
describe('UpdatePartial', () => {
let update: UpdatePartial;
describe('Checking Constraints', () => {
beforeEach(() => {
update = new UpdatePartial();
});
// FAIL
it('should fail if url is not uuid', async () => {
update.url = 'asdf';
const results = await validate(update);
expect(results.length).toBe(1); // should detect IsUUID. But fails
});
// PASS
it('it should pass if update is empty', async () => {
const results = await validate(update);
expect(results.length).toBe(0);
});
});
});
I know that mapped-types is a NestJS lib, but the behavior is similar.
Expected behavior
- Same behavior on version v0.11.1: inheritance during overriding should mantain the validations, no matter what the inheritance depth.
Actual behavior
- overriding methods are overriding validations, and validations are being removed.
Related
- Related Issue: bug: Inheritance and overriding fields #2608
- gist
- test/functional/inherited-validation.spec.ts
- current version v0.14.2; last version when it worked: v0.11.1.