diff --git a/.changeset/lovely-bobcats-hang.md b/.changeset/lovely-bobcats-hang.md
new file mode 100644
index 00000000..a32fd203
--- /dev/null
+++ b/.changeset/lovely-bobcats-hang.md
@@ -0,0 +1,7 @@
+---
+'@typeschema/class-validator': minor
+'@typeschema/main': patch
+'@typeschema/all': patch
+---
+
+Improved error messages for class-validator
diff --git a/README.md b/README.md
index b17ee3e3..53023772 100644
--- a/README.md
+++ b/README.md
@@ -124,16 +124,6 @@ We value flexibility, which is why there are multiple ways of using TypeSchema:
@typeschema/class-validator |
|
-
- superstruct |
- |
- ✅ |
- 🧐 |
- ✅ |
- 🧐 |
- @typeschema/superstruct |
- |
-
effect |
|
@@ -144,6 +134,16 @@ We value flexibility, which is why there are multiple ways of using TypeSchema:
@typeschema/effect |
|
+
+ superstruct |
+ |
+ ✅ |
+ 🧐 |
+ ✅ |
+ 🧐 |
+ @typeschema/superstruct |
+ |
+
io-ts |
|
@@ -185,24 +185,24 @@ We value flexibility, which is why there are multiple ways of using TypeSchema:
|
- ow |
- |
+ arktype |
+ |
✅ |
✅ |
✅ |
🧐 |
- @typeschema/ow |
- |
+ @typeschema/arktype |
+ |
- arktype |
- |
+ ow |
+ |
✅ |
✅ |
✅ |
🧐 |
- @typeschema/arktype |
- |
+ @typeschema/ow |
+ |
deepkit |
diff --git a/packages/all/src/__tests__/class-validator.test.ts b/packages/all/src/__tests__/class-validator.test.ts
index e2867c3d..c6454007 100644
--- a/packages/all/src/__tests__/class-validator.test.ts
+++ b/packages/all/src/__tests__/class-validator.test.ts
@@ -10,8 +10,11 @@ import {
IsEmail,
IsInt,
IsNotEmpty,
+ IsOptional,
+ IsString,
IsUUID,
Min,
+ ValidateNested,
} from 'class-validator';
import {expectTypeOf} from 'expect-type';
import {describe, expect, test} from 'vitest';
@@ -19,6 +22,12 @@ import {describe, expect, test} from 'vitest';
import {assert, validate, wrap} from '..';
describe('class-validator', () => {
+ class NestedSchema {
+ @IsString()
+ @IsNotEmpty()
+ value!: string;
+ }
+
class Schema {
@IsInt()
@Min(0)
@@ -38,6 +47,14 @@ describe('class-validator', () => {
@IsDateString()
updatedAt!: string;
+
+ @IsOptional()
+ @ValidateNested()
+ nested?: NestedSchema;
+
+ @IsOptional()
+ @ValidateNested()
+ nestedArray?: Array;
}
const schema = Schema;
@@ -48,7 +65,8 @@ describe('class-validator', () => {
id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74',
name: 'John Doe',
updatedAt: '2021-01-01T00:00:00.000Z',
- };
+ } as Schema;
+
const badData = {
age: '123',
createdAt: '2021-01-01T00:00:00.000Z',
@@ -58,6 +76,17 @@ describe('class-validator', () => {
updatedAt: '2021-01-01T00:00:00.000Z',
};
+ const badNestedData = {
+ age: 123,
+ createdAt: '2021-01-01T00:00:00.000Z',
+ email: 'john.doe@test.com',
+ id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74',
+ name: 'John Doe',
+ updatedAt: '2021-01-01T00:00:00.000Z',
+ nested: new NestedSchema(),
+ nestedArray: [new NestedSchema()],
+ };
+
test('infer', () => {
expectTypeOf>().toEqualTypeOf(data);
expectTypeOf>().toEqualTypeOf(data);
@@ -71,11 +100,35 @@ describe('class-validator', () => {
expect(await validate(schema, badData)).toStrictEqual({
issues: [
{
- message: `An instance of Schema has failed the validation:
- - property age has failed the following constraints: min, isInt
-`,
+ message: 'age must not be less than 0',
path: ['age'],
},
+ {
+ message: 'age must be an integer number',
+ path: ['age'],
+ },
+ ],
+ success: false,
+ });
+
+ expect(await validate(schema, badNestedData)).toStrictEqual({
+ issues: [
+ {
+ message: 'value should not be empty',
+ path: ['nested', 'value'],
+ },
+ {
+ message: 'value must be a string',
+ path: ['nested', 'value'],
+ },
+ {
+ message: 'value should not be empty',
+ path: ['nestedArray', 0, 'value'],
+ },
+ {
+ message: 'value must be a string',
+ path: ['nestedArray', 0, 'value'],
+ },
],
success: false,
});
diff --git a/packages/class-validator/src/__tests__/class-validator.test.ts b/packages/class-validator/src/__tests__/class-validator.test.ts
index 5bccd92c..21da4ae3 100644
--- a/packages/class-validator/src/__tests__/class-validator.test.ts
+++ b/packages/class-validator/src/__tests__/class-validator.test.ts
@@ -44,11 +44,13 @@ describe('class-validator', () => {
@IsDateString()
updatedAt!: string;
+ @IsOptional()
@ValidateNested()
- nested!: NestedSchema;
+ nested?: NestedSchema;
+
@IsOptional()
@ValidateNested()
- nestedArray?: NestedSchema[];
+ nestedArray?: Array;
}
const schema = Schema;
@@ -67,7 +69,7 @@ describe('class-validator', () => {
email: 'john.doe@test.com',
id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74',
name: 'John Doe',
- updatedAt: '2021-01-01T00:00:00.000Z'
+ updatedAt: '2021-01-01T00:00:00.000Z',
};
const badNestedData = {
@@ -100,7 +102,7 @@ describe('class-validator', () => {
{
message: 'age must be an integer number',
path: ['age'],
- }
+ },
],
success: false,
});
@@ -109,20 +111,20 @@ describe('class-validator', () => {
issues: [
{
message: 'value should not be empty',
- path: ['nested.value'],
+ path: ['nested', 'value'],
},
{
message: 'value must be a string',
- path: ['nested.value'],
+ path: ['nested', 'value'],
},
{
message: 'value should not be empty',
- path: ['nestedArray[0].value'],
+ path: ['nestedArray', 0, 'value'],
},
{
message: 'value must be a string',
- path: ['nestedArray[0].value'],
- }
+ path: ['nestedArray', 0, 'value'],
+ },
],
success: false,
});
diff --git a/packages/class-validator/src/validation.ts b/packages/class-validator/src/validation.ts
index 4d699f64..8ff885aa 100644
--- a/packages/class-validator/src/validation.ts
+++ b/packages/class-validator/src/validation.ts
@@ -1,36 +1,34 @@
-/* eslint-disable prettier/prettier */
import type {AdapterResolver} from './resolver';
import type {ValidationAdapter, ValidationIssue} from '@typeschema/core';
import {memoize} from '@typeschema/core';
-import { ValidationError } from "class-validator";
+import {ValidationError} from 'class-validator';
const importValidationModule = memoize(async () => {
const {validate} = await import('class-validator');
return {validate};
});
+function getIssues(
+ error: ValidationError,
+ parentPath: Array,
+): Array {
+ const path = [
+ ...parentPath,
+ Number.isInteger(+error.property) ? +error.property : error.property,
+ ];
+ return Object.values(error.constraints ?? {})
+ .map((message): ValidationIssue => ({message, path}))
+ .concat(
+ error.children?.flatMap(childError => getIssues(childError, path)) ?? [],
+ );
+}
+
export const validationAdapter: ValidationAdapter<
AdapterResolver
> = async schema => {
const {validate} = await importValidationModule();
return async data => {
- function getIssues(error: ValidationError, parentPath = ""): ValidationIssue[] {
- const currentPath = parentPath
- ? Number.isInteger(+error.property) ? `${parentPath}[${error.property}]` : `${parentPath}.${error.property}`
- : error.property;
- const constraints = error.constraints ? Object.values(error.constraints) : [];
- const childIssues = error.children ? error.children.flatMap(childError => getIssues(childError, currentPath)) : [];
-
- return [
- ...constraints.map((message) => ({
- message: message,
- path: [currentPath],
- })),
- ...childIssues
- ];
- }
-
const errors = await validate(Object.assign(new schema(), data));
if (errors.length === 0) {
return {
@@ -40,7 +38,7 @@ export const validationAdapter: ValidationAdapter<
};
}
return {
- issues: errors.flatMap(error => getIssues(error)),
+ issues: errors.flatMap(error => getIssues(error, [])),
success: false,
};
};
diff --git a/packages/main/src/__tests__/class-validator.test.ts b/packages/main/src/__tests__/class-validator.test.ts
index e2867c3d..c6454007 100644
--- a/packages/main/src/__tests__/class-validator.test.ts
+++ b/packages/main/src/__tests__/class-validator.test.ts
@@ -10,8 +10,11 @@ import {
IsEmail,
IsInt,
IsNotEmpty,
+ IsOptional,
+ IsString,
IsUUID,
Min,
+ ValidateNested,
} from 'class-validator';
import {expectTypeOf} from 'expect-type';
import {describe, expect, test} from 'vitest';
@@ -19,6 +22,12 @@ import {describe, expect, test} from 'vitest';
import {assert, validate, wrap} from '..';
describe('class-validator', () => {
+ class NestedSchema {
+ @IsString()
+ @IsNotEmpty()
+ value!: string;
+ }
+
class Schema {
@IsInt()
@Min(0)
@@ -38,6 +47,14 @@ describe('class-validator', () => {
@IsDateString()
updatedAt!: string;
+
+ @IsOptional()
+ @ValidateNested()
+ nested?: NestedSchema;
+
+ @IsOptional()
+ @ValidateNested()
+ nestedArray?: Array;
}
const schema = Schema;
@@ -48,7 +65,8 @@ describe('class-validator', () => {
id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74',
name: 'John Doe',
updatedAt: '2021-01-01T00:00:00.000Z',
- };
+ } as Schema;
+
const badData = {
age: '123',
createdAt: '2021-01-01T00:00:00.000Z',
@@ -58,6 +76,17 @@ describe('class-validator', () => {
updatedAt: '2021-01-01T00:00:00.000Z',
};
+ const badNestedData = {
+ age: 123,
+ createdAt: '2021-01-01T00:00:00.000Z',
+ email: 'john.doe@test.com',
+ id: 'c4a760a8-dbcf-4e14-9f39-645a8e933d74',
+ name: 'John Doe',
+ updatedAt: '2021-01-01T00:00:00.000Z',
+ nested: new NestedSchema(),
+ nestedArray: [new NestedSchema()],
+ };
+
test('infer', () => {
expectTypeOf>().toEqualTypeOf(data);
expectTypeOf>().toEqualTypeOf(data);
@@ -71,11 +100,35 @@ describe('class-validator', () => {
expect(await validate(schema, badData)).toStrictEqual({
issues: [
{
- message: `An instance of Schema has failed the validation:
- - property age has failed the following constraints: min, isInt
-`,
+ message: 'age must not be less than 0',
path: ['age'],
},
+ {
+ message: 'age must be an integer number',
+ path: ['age'],
+ },
+ ],
+ success: false,
+ });
+
+ expect(await validate(schema, badNestedData)).toStrictEqual({
+ issues: [
+ {
+ message: 'value should not be empty',
+ path: ['nested', 'value'],
+ },
+ {
+ message: 'value must be a string',
+ path: ['nested', 'value'],
+ },
+ {
+ message: 'value should not be empty',
+ path: ['nestedArray', 0, 'value'],
+ },
+ {
+ message: 'value must be a string',
+ path: ['nestedArray', 0, 'value'],
+ },
],
success: false,
});