Skip to content

Commit 3bb6a50

Browse files
committed
feat(common): add error format option to validation pipe
Add a new `errorFormat` option to `ValidationPipeOptions` that allows users to choose between two validation error formats: - 'list' (default): Returns an array of error message strings with parent path prepended to messages (current behavior) - 'grouped': Returns an object with property paths as keys and arrays of unmodified constraint messages as values The 'grouped' format separates property paths from error messages, which prevents custom validation messages from being modified with parent path prefixes. Closes #16268
1 parent 782e071 commit 3bb6a50

File tree

2 files changed

+79
-0
lines changed

2 files changed

+79
-0
lines changed

packages/common/pipes/validation.pipe.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import {
2020
import { loadPackage } from '../utils/load-package.util';
2121
import { isNil, isUndefined } from '../utils/shared.utils';
2222

23+
/**
24+
* @publicApi
25+
*/
26+
export type ValidationErrorFormat = 'list' | 'grouped';
27+
2328
/**
2429
* @publicApi
2530
*/
@@ -33,6 +38,12 @@ export interface ValidationPipeOptions extends ValidatorOptions {
3338
expectedType?: Type<any>;
3439
validatorPackage?: ValidatorPackage;
3540
transformerPackage?: TransformerPackage;
41+
/**
42+
* Specifies the format of validation error messages.
43+
* - 'list': Returns an array of error message strings (default)
44+
* - 'grouped': Returns an object with property paths as keys and arrays of error messages as values
45+
*/
46+
errorFormat?: ValidationErrorFormat;
3647
}
3748

3849
let classValidator: ValidatorPackage = {} as any;
@@ -59,6 +70,7 @@ export class ValidationPipe implements PipeTransform<any> {
5970
protected expectedType: Type<any> | undefined;
6071
protected exceptionFactory: (errors: ValidationError[]) => any;
6172
protected validateCustomDecorators: boolean;
73+
protected errorFormat: ValidationErrorFormat;
6274

6375
constructor(@Optional() options?: ValidationPipeOptions) {
6476
options = options || {};
@@ -69,6 +81,7 @@ export class ValidationPipe implements PipeTransform<any> {
6981
expectedType,
7082
transformOptions,
7183
validateCustomDecorators,
84+
errorFormat,
7285
...validatorOptions
7386
} = options;
7487

@@ -81,6 +94,7 @@ export class ValidationPipe implements PipeTransform<any> {
8194
this.validateCustomDecorators = validateCustomDecorators || false;
8295
this.errorHttpStatusCode = errorHttpStatusCode || HttpStatus.BAD_REQUEST;
8396
this.expectedType = expectedType;
97+
this.errorFormat = errorFormat || 'list';
8498
this.exceptionFactory =
8599
options.exceptionFactory || this.createExceptionFactory();
86100

@@ -183,6 +197,12 @@ export class ValidationPipe implements PipeTransform<any> {
183197
if (this.isDetailedOutputDisabled) {
184198
return new HttpErrorByCode[this.errorHttpStatusCode]();
185199
}
200+
if (this.errorFormat === 'grouped') {
201+
const errors = this.groupValidationErrors(validationErrors);
202+
return new HttpErrorByCode[this.errorHttpStatusCode]({
203+
message: errors,
204+
});
205+
}
186206
const errors = this.flattenValidationErrors(validationErrors);
187207
return new HttpErrorByCode[this.errorHttpStatusCode](errors);
188208
};
@@ -310,6 +330,25 @@ export class ValidationPipe implements PipeTransform<any> {
310330
.toArray();
311331
}
312332

333+
protected groupValidationErrors(
334+
validationErrors: ValidationError[],
335+
parentPath?: string,
336+
): Record<string, string[]> {
337+
const result: Record<string, string[]> = {};
338+
for (const error of validationErrors) {
339+
const path = parentPath
340+
? `${parentPath}.${error.property}`
341+
: error.property;
342+
if (error.constraints) {
343+
result[path] = Object.values(error.constraints);
344+
}
345+
if (error.children && error.children.length) {
346+
Object.assign(result, this.groupValidationErrors(error.children, path));
347+
}
348+
}
349+
return result;
350+
}
351+
313352
protected mapChildrenToValidationErrors(
314353
error: ValidationError,
315354
parentPath?: string,

packages/common/test/pipes/validation.pipe.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,46 @@ describe('ValidationPipe', () => {
183183
]);
184184
}
185185
});
186+
187+
describe('when errorFormat is "grouped"', () => {
188+
beforeEach(() => {
189+
target = new ValidationPipe({ errorFormat: 'grouped' });
190+
});
191+
192+
it('should return grouped errors with property paths as keys', async () => {
193+
try {
194+
const model = new TestModelWithNested();
195+
model.test = new TestModel2();
196+
await target.transform(model, {
197+
type: 'body',
198+
metatype: TestModelWithNested,
199+
});
200+
} catch (err) {
201+
expect(err.getResponse().message).to.be.eql({
202+
prop: ['prop must be a string'],
203+
'test.prop1': ['prop1 must be a string'],
204+
'test.prop2': ['prop2 must be a boolean value'],
205+
});
206+
}
207+
});
208+
209+
it('should return grouped errors for nested arrays', async () => {
210+
try {
211+
const model = new TestModelForNestedArrayValidation();
212+
model.test = [new TestModel2()];
213+
await target.transform(model, {
214+
type: 'body',
215+
metatype: TestModelForNestedArrayValidation,
216+
});
217+
} catch (err) {
218+
expect(err.getResponse().message).to.be.eql({
219+
prop: ['prop must be a string'],
220+
'test.0.prop1': ['prop1 must be a string'],
221+
'test.0.prop2': ['prop2 must be a boolean value'],
222+
});
223+
}
224+
});
225+
});
186226
});
187227
describe('when validation transforms', () => {
188228
it('should return a TestModel instance', async () => {

0 commit comments

Comments
 (0)