Repository: react-hook-form/resolvers (ts)

Issue:
Ajv resolver not using custom error messages on required errors
**Describe the bug**
Without a custom required error message set on my AJV form schema, required form field errors are returned as expected with the default AJV required error message. With a custom required error message set, no error is set on the form field. The required error message is attached to the error body on a non existent `""` form field

**To Reproduce**
Steps to reproduce the behaviour:

Set the form schema: 
```
{
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "email": {
      "type": "string"
    }
  },
  "required": ["name", "email"],
  "errorMessage": {
    "required": {
      "name": "Name must be defined",
      "email": "Email must be defined"
    }
  }
}
```

Set the form value:
```
{
  "name": undefined,
  "email": undefined
}
```

Expected react hook form error when both name and email are not defined:
```
{
  "name": {
    "message": "Name must be defined",
    "type": "required"
  },
  "email": {
    "message": "Email must be defined",
    "type": "required"
  }
}
```

Actual error returned:
```
{
  "": { "message": "Name must be defined", "type": "errorMessage" }
}
```

**Codesandbox link (Required)**

[Example](https://codesandbox.io/s/react-hook-form-ajvresolver-forked-tk32wy)

First name is using the default ajv error messages. Last name is using a custom required error message. On submitting the form, only the required first name error message is displayed.

**Expected behavior**
Custom required error messages should be returned by form errors on the relevant form field.

**Desktop (please complete the following information):**

- OS: macOS Ventura v13.4
- Browser Chrome
- Version 115.0.5790.170


Pull request:
feat(ajv): Keep original validation type while using `errorMessage`
When `errorMessage` is used, ajv returns a special error object, `error.keyword='errorMessage'`, and keeps original validation errors in the `error.errors` field.

The proposed changes normalize the ajv validation errors by unwrapping the `errorMessage` error object, so the resolver returns original error types, such as required and pattern, while using custom error messages. The normalization also allows us to use `errorMessage`  at the root level (fixes #615).

As you can see in the before and after outputs, the value of the `type` field and the keys of the `types` field use the original validation type instead of `errorMessage`.

**Before**

```json
  "password": {
    "message": "One uppercase character",
    "ref": {
      "name": "password",
    },
    "type": "errorMessage",
    "types": {
      "errorMessage": "One uppercase character",
    },
  },
```

**After**

```json
  "password": {
    "message": "One uppercase character",
    "ref": {
      "name": "password",
    },
    "type": "pattern",
    "types": {
      "pattern": "One uppercase character",
    },
  },
```

**Example schema**

```ts
const schema = {
  type: "object",
  properties: {
    username: {
      type: "string",
      minLength: 3,
      errorMessage: {
        minLength: "username should be at least three characters long",
      },
    },
    password: {
      type: "string",
      pattern: ".*[A-Z].*",
      minLength: 8,
      errorMessage: {
        pattern: "One uppercase character",
        minLength: "passwords should be at least eight characters long",
      },
    },
  },
  required: ["username", "password"],
  additionalProperties: false,
  errorMessage: {
    required: {
      username: "username field is required",
      password: "password field is required",
    },
  },
};
```

**Input Value**

```ts
const value = {
  username: "kt",
  password: "invalid",
};
```

**Resolver Output**

```json
{
  "errors": {
    "password": {
      "message": "One uppercase character",
      "ref": {
        "name": "password"
      },
      "type": "pattern",
      "types": {
        "minLength": "passwords should be at least eight characters long",
        "pattern": "One uppercase character"
      }
    },
    "username": {
      "message": "username should be at least three characters long",
      "ref": {
        "name": "username"
      },
      "type": "minLength",
      "types": {
        "minLength": "username should be at least three characters long"
      }
    }
  },
  "values": {}
}
```

**Input Value**

```ts
const value = {};
```

**Resolver Output**

```json
{
  "errors": {
    "password": {
      "message": "password field is required",
      "ref": {
        "name": "password"
      },
      "type": "required",
      "types": {
        "required": "password field is required"
      }
    },
    "username": {
      "message": "username field is required",
      "ref": {
        "name": "username"
      },
      "type": "required",
      "types": {
        "required": "username field is required"
      }
    }
  },
  "values": {}
}
```


Patch:
diff --git a/README.md b/README.md
index b0273b5..9f9fefa 100644
--- a/README.md
+++ b/README.md
@@ -670,7 +670,7 @@ const schema = Schema.Struct({
   ),
 });
 
-type FormData = Schema.Schema.Type<typeof schema>;
+type FormData = typeof schema.Type;
 
 interface Props {
   onSubmit: (data: FormData) => void;
diff --git a/ajv/src/ajv.ts b/ajv/src/ajv.ts
index 19146a1..ba55d5a 100644
--- a/ajv/src/ajv.ts
+++ b/ajv/src/ajv.ts
@@ -2,47 +2,60 @@ import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers';
 import Ajv, { DefinedError } from 'ajv';
 import ajvErrors from 'ajv-errors';
 import { FieldError, appendErrors } from 'react-hook-form';
-import { Resolver } from './types';
+import { AjvError, Resolver } from './types';
 
 const parseErrorSchema = (
-  ajvErrors: DefinedError[],
+  ajvErrors: AjvError[],
   validateAllFieldCriteria: boolean,
 ) => {
-  // Ajv will return empty instancePath when require error
-  ajvErrors.forEach((error) => {
+  const parsedErrors: Record<string, FieldError> = {};
+
+  const reduceError = (error: AjvError) => {
+    // Ajv will return empty instancePath when require error
     if (error.keyword === 'required') {
-      error.instancePath += '/' + error.params.missingProperty;
+      error.instancePath += `/${error.params.missingProperty}`;
     }
-  });
 
-  return ajvErrors.reduce<Record<string, FieldError>>((previous, error) => {
     // `/deepObject/data` -> `deepObject.data`
     const path = error.instancePath.substring(1).replace(/\//g, '.');
 
-    if (!previous[path]) {
-      previous[path] = {
+    if (!parsedErrors[path]) {
+      parsedErrors[path] = {
         message: error.message,
         type: error.keyword,
       };
     }
 
     if (validateAllFieldCriteria) {
-      const types = previous[path].types;
+      const types = parsedErrors[path].types;
       const messages = types && types[error.keyword];
 
-      previous[path] = appendErrors(
+      parsedErrors[path] = appendErrors(
         path,
         validateAllFieldCriteria,
-        previous,
+        parsedErrors,
         error.keyword,
         messages
           ? ([] as string[]).concat(messages as string[], error.message || '')
           : error.message,
       ) as FieldError;
     }
+  };
+
+  for (let index = 0; index < ajvErrors.length; index += 1) {
+    const error = ajvErrors[index];
+
+    if (error.keyword === 'errorMessage') {
+      error.params.errors.forEach((originalError) => {
+        originalError.message = error.message;
+        reduceError(originalError);
+      });
+    } else {
+      reduceError(error);
+    }
+  }
 
-    return previous;
-  }, {});
+  return parsedErrors;
 };
 
 export const ajvResolver: Resolver =
diff --git a/ajv/src/types.ts b/ajv/src/types.ts
index 2defc4d..6726ccd 100644
--- a/ajv/src/types.ts
+++ b/ajv/src/types.ts
@@ -1,4 +1,5 @@
 import * as Ajv from 'ajv';
+import type { DefinedError, ErrorObject } from 'ajv';
 import { FieldValues, ResolverOptions, ResolverResult } from 'react-hook-form';
 
 export type Resolver = <T>(
@@ -10,3 +11,10 @@ export type Resolver = <T>(
   context: TContext | undefined,
   options: ResolverOptions<TFieldValues>,
 ) => Promise<ResolverResult<TFieldValues>>;
+
+// ajv doesn't export any types for errors with `keyword='errorMessage'`
+type ErrorMessage = ErrorObject<
+  'errorMessage',
+  { errors: (DefinedError & { emUsed: boolean })[] }
+>;
+export type AjvError = ErrorMessage | DefinedError;
diff --git a/arktype/package.json b/arktype/package.json
index e7413e0..d3fe5ae 100644
--- a/arktype/package.json
+++ b/arktype/package.json
@@ -13,6 +13,6 @@
   "peerDependencies": {
     "react-hook-form": "^7.0.0",
     "@hookform/resolvers": "^2.0.0",
-    "arktype": "2.0.0-dev.14"
+    "arktype": "^2.0.0"
   }
 }
diff --git a/src/toNestErrors.ts b/src/toNestErrors.ts
index 0aa71a8..454f7d4 100644
--- a/src/toNestErrors.ts
+++ b/src/toNestErrors.ts
@@ -38,4 +38,4 @@ export const toNestErrors = <TFieldValues extends FieldValues>(
 const isNameInFieldArray = (
   names: InternalFieldName[],
   name: InternalFieldName,
-) => names.some((n) => n.startsWith(name + '.'));
+) => names.some((n) => n.match(`^${name}\\.\\d+`));


Test patch:
diff --git a/ajv/src/__tests__/__fixtures__/data-errors.ts b/ajv/src/__tests__/__fixtures__/data-errors.ts
new file mode 100644
index 0000000..a5b3e57
--- /dev/null
+++ b/ajv/src/__tests__/__fixtures__/data-errors.ts
@@ -0,0 +1,216 @@
+import { JSONSchemaType } from 'ajv';
+import { Field, InternalFieldName } from 'react-hook-form';
+
+interface DataA {
+  username: string;
+  password: string;
+}
+
+export const schemaA: JSONSchemaType<DataA> = {
+  type: 'object',
+  properties: {
+    username: {
+      type: 'string',
+      minLength: 3,
+      errorMessage: {
+        minLength: 'username should be at least three characters long',
+      },
+    },
+    password: {
+      type: 'string',
+      pattern: '.*[A-Z].*',
+      minLength: 8,
+      errorMessage: {
+        pattern: 'One uppercase character',
+        minLength: 'passwords should be at least eight characters long',
+      },
+    },
+  },
+  required: ['username', 'password'],
+  additionalProperties: false,
+  errorMessage: {
+    required: {
+      username: 'username field is required',
+      password: 'password field is required',
+    },
+  },
+};
+
+export const validDataA: DataA = {
+  username: 'kt666',
+  password: 'validPassword',
+};
+
+export const invalidDataA = {
+  username: 'kt',
+  password: 'invalid',
+};
+
+export const undefinedDataA = {
+  username: undefined,
+  password: undefined,
+};
+
+export const fieldsA: Record<InternalFieldName, Field['_f']> = {
+  username: {
+    ref: { name: 'username' },
+    name: 'username',
+  },
+  password: {
+    ref: { name: 'password' },
+    name: 'password',
+  },
+  email: {
+    ref: { name: 'email' },
+    name: 'email',
+  },
+  birthday: {
+    ref: { name: 'birthday' },
+    name: 'birthday',
+  },
+};
+
+// examples from [ajv-errors](https://github.com/ajv-validator/ajv-errors)
+
+interface DataB {
+  foo: number;
+}
+
+export const schemaB: JSONSchemaType<DataB> = {
+  type: 'object',
+  required: ['foo'],
+  properties: {
+    foo: { type: 'integer' },
+  },
+  additionalProperties: false,
+  errorMessage: 'should be an object with an integer property foo only',
+};
+
+export const validDataB: DataB = { foo: 666 };
+export const invalidDataB = { foo: 'kt', bar: 6 };
+export const undefinedDataB = { foo: undefined };
+
+interface DataC {
+  foo: number;
+}
+
+export const schemaC: JSONSchemaType<DataC> = {
+  type: 'object',
+  required: ['foo'],
+  properties: {
+    foo: { type: 'integer' },
+  },
+  additionalProperties: false,
+  errorMessage: {
+    type: 'should be an object',
+    required: 'should have property foo',
+    additionalProperties: 'should not have properties other than foo',
+  },
+};
+
+export const validDataC: DataC = { foo: 666 };
+export const invalidDataC = { foo: 'kt', bar: 6 };
+export const undefinedDataC = { foo: undefined };
+export const invalidTypeDataC = 'something';
+
+interface DataD {
+  foo: number;
+  bar: string;
+}
+
+export const schemaD: JSONSchemaType<DataD> = {
+  type: 'object',
+  required: ['foo', 'bar'],
+  properties: {
+    foo: { type: 'integer' },
+    bar: { type: 'string' },
+  },
+  errorMessage: {
+    type: 'should be an object', // will not replace internal "type" error for the property "foo"
+    required: {
+      foo: 'should have an integer property "foo"',
+      bar: 'should have a string property "bar"',
+    },
+  },
+};
+
+export const validDataD: DataD = { foo: 666, bar: 'kt' };
+export const invalidDataD = { foo: 'kt', bar: 6 };
+export const undefinedDataD = { foo: undefined, bar: undefined };
+export const invalidTypeDataD = 'something';
+
+interface DataE {
+  foo: number;
+  bar: string;
+}
+
+export const schemaE: JSONSchemaType<DataE> = {
+  type: 'object',
+  required: ['foo', 'bar'],
+  allOf: [
+    {
+      properties: {
+        foo: { type: 'integer', minimum: 2 },
+        bar: { type: 'string', minLength: 2 },
+      },
+      additionalProperties: false,
+    },
+  ],
+  errorMessage: {
+    properties: {
+      foo: 'data.foo should be integer >= 2',
+      bar: 'data.bar should be string with length >= 2',
+    },
+  },
+};
+
+export const validDataE: DataE = { foo: 666, bar: 'kt' };
+export const invalidDataE = { foo: 1, bar: 'k' };
+export const undefinedDataE = { foo: undefined, bar: undefined };
+
+interface DataF {
+  foo: number;
+  bar: string;
+}
+
+export const schemaF: JSONSchemaType<DataF> = {
+  type: 'object',
+  required: ['foo', 'bar'],
+  allOf: [
+    {
+      properties: {
+        foo: { type: 'integer', minimum: 2 },
+        bar: { type: 'string', minLength: 2 },
+      },
+      additionalProperties: false,
+    },
+  ],
+  errorMessage: {
+    type: 'data should be an object',
+    properties: {
+      foo: 'data.foo should be integer >= 2',
+      bar: 'data.bar should be string with length >= 2',
+    },
+    _: 'data should have properties "foo" and "bar" only',
+  },
+};
+
+export const validDataF: DataF = { foo: 666, bar: 'kt' };
+export const invalidDataF = {};
+export const undefinedDataF = { foo: 1, bar: undefined };
+export const invalidTypeDataF = 'something';
+
+export const fieldsRest: Record<InternalFieldName, Field['_f']> = {
+  foo: {
+    ref: { name: 'foo' },
+    name: 'foo',
+  },
+  bar: {
+    ref: { name: 'bar' },
+    name: 'bar',
+  },
+  lorem: {
+    ref: { name: 'lorem' },
+    name: 'lorem',
+  },
+};
diff --git a/ajv/src/__tests__/__snapshots__/ajv-errors.ts.snap b/ajv/src/__tests__/__snapshots__/ajv-errors.ts.snap
new file mode 100644
index 0000000..56062b2
--- /dev/null
+++ b/ajv/src/__tests__/__snapshots__/ajv-errors.ts.snap
@@ -0,0 +1,462 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`ajvResolver with errorMessage > should return a default message if there is no specific message for the error when requirement fails 1`] = `
+{
+  "errors": {
+    "bar": {
+      "message": "data should have properties "foo" and "bar" only",
+      "ref": {
+        "name": "bar",
+      },
+      "type": "required",
+      "types": {
+        "required": "data should have properties "foo" and "bar" only",
+      },
+    },
+    "foo": {
+      "message": "data should have properties "foo" and "bar" only",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "required",
+      "types": {
+        "required": "data should have properties "foo" and "bar" only",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return a default message if there is no specific message for the error when some properties are undefined 1`] = `
+{
+  "errors": {
+    "bar": {
+      "message": "data should have properties "foo" and "bar" only",
+      "ref": {
+        "name": "bar",
+      },
+      "type": "required",
+      "types": {
+        "required": "data should have properties "foo" and "bar" only",
+      },
+    },
+    "foo": {
+      "message": "data.foo should be integer >= 2",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "minimum",
+      "types": {
+        "minimum": "data.foo should be integer >= 2",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return a default message if there is no specific message for the error when walidation fails 1`] = `
+{
+  "errors": {
+    "bar": {
+      "message": "data should have properties "foo" and "bar" only",
+      "ref": {
+        "name": "bar",
+      },
+      "type": "required",
+      "types": {
+        "required": "data should have properties "foo" and "bar" only",
+      },
+    },
+    "foo": {
+      "message": "data should have properties "foo" and "bar" only",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "required",
+      "types": {
+        "required": "data should have properties "foo" and "bar" only",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return customized error messages for certain keywords when requirement fails 1`] = `
+{
+  "errors": {
+    "foo": {
+      "message": "should have property foo",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "required",
+      "types": {
+        "required": "should have property foo",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return customized error messages for certain keywords when some properties are undefined 1`] = `
+{
+  "errors": {
+    "foo": {
+      "message": "should have property foo",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "required",
+      "types": {
+        "required": "should have property foo",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return customized error messages for certain keywords when walidation fails 1`] = `
+{
+  "errors": {
+    "": {
+      "message": "should not have properties other than foo",
+      "ref": undefined,
+      "type": "additionalProperties",
+      "types": {
+        "additionalProperties": "should not have properties other than foo",
+      },
+    },
+    "foo": {
+      "message": "must be integer",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "type",
+      "types": {
+        "type": "must be integer",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return customized error messages when requirement fails 1`] = `
+{
+  "errors": {
+    "password": {
+      "message": "password field is required",
+      "ref": {
+        "name": "password",
+      },
+      "type": "required",
+      "types": {
+        "required": "password field is required",
+      },
+    },
+    "username": {
+      "message": "username field is required",
+      "ref": {
+        "name": "username",
+      },
+      "type": "required",
+      "types": {
+        "required": "username field is required",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return customized error messages when some properties are undefined 1`] = `
+{
+  "errors": {
+    "password": {
+      "message": "password field is required",
+      "ref": {
+        "name": "password",
+      },
+      "type": "required",
+      "types": {
+        "required": "password field is required",
+      },
+    },
+    "username": {
+      "message": "username field is required",
+      "ref": {
+        "name": "username",
+      },
+      "type": "required",
+      "types": {
+        "required": "username field is required",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return customized error messages when validation fails 1`] = `
+{
+  "errors": {
+    "password": {
+      "message": "One uppercase character",
+      "ref": {
+        "name": "password",
+      },
+      "type": "pattern",
+      "types": {
+        "minLength": "passwords should be at least eight characters long",
+        "pattern": "One uppercase character",
+      },
+    },
+    "username": {
+      "message": "username should be at least three characters long",
+      "ref": {
+        "name": "username",
+      },
+      "type": "minLength",
+      "types": {
+        "minLength": "username should be at least three characters long",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return customized errors for properties/items when requirement fails 1`] = `
+{
+  "errors": {
+    "bar": {
+      "message": "must have required property 'bar'",
+      "ref": {
+        "name": "bar",
+      },
+      "type": "required",
+      "types": {
+        "required": "must have required property 'bar'",
+      },
+    },
+    "foo": {
+      "message": "must have required property 'foo'",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "required",
+      "types": {
+        "required": "must have required property 'foo'",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return customized errors for properties/items when some properties are undefined 1`] = `
+{
+  "errors": {
+    "bar": {
+      "message": "must have required property 'bar'",
+      "ref": {
+        "name": "bar",
+      },
+      "type": "required",
+      "types": {
+        "required": "must have required property 'bar'",
+      },
+    },
+    "foo": {
+      "message": "must have required property 'foo'",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "required",
+      "types": {
+        "required": "must have required property 'foo'",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return customized errors for properties/items when walidation fails 1`] = `
+{
+  "errors": {
+    "bar": {
+      "message": "data.bar should be string with length >= 2",
+      "ref": {
+        "name": "bar",
+      },
+      "type": "minLength",
+      "types": {
+        "minLength": "data.bar should be string with length >= 2",
+      },
+    },
+    "foo": {
+      "message": "data.foo should be integer >= 2",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "minimum",
+      "types": {
+        "minimum": "data.foo should be integer >= 2",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return different messages for different properties when requirement fails 1`] = `
+{
+  "errors": {
+    "bar": {
+      "message": "should have a string property "bar"",
+      "ref": {
+        "name": "bar",
+      },
+      "type": "required",
+      "types": {
+        "required": "should have a string property "bar"",
+      },
+    },
+    "foo": {
+      "message": "should have an integer property "foo"",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "required",
+      "types": {
+        "required": "should have an integer property "foo"",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return different messages for different properties when some properties are undefined 1`] = `
+{
+  "errors": {
+    "bar": {
+      "message": "should have a string property "bar"",
+      "ref": {
+        "name": "bar",
+      },
+      "type": "required",
+      "types": {
+        "required": "should have a string property "bar"",
+      },
+    },
+    "foo": {
+      "message": "should have an integer property "foo"",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "required",
+      "types": {
+        "required": "should have an integer property "foo"",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return different messages for different properties when walidation fails 1`] = `
+{
+  "errors": {
+    "bar": {
+      "message": "must be string",
+      "ref": {
+        "name": "bar",
+      },
+      "type": "type",
+      "types": {
+        "type": "must be string",
+      },
+    },
+    "foo": {
+      "message": "must be integer",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "type",
+      "types": {
+        "type": "must be integer",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return the same customized error message when requirement fails 1`] = `
+{
+  "errors": {
+    "foo": {
+      "message": "should be an object with an integer property foo only",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "required",
+      "types": {
+        "required": "should be an object with an integer property foo only",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return the same customized message for all validation failures 1`] = `
+{
+  "errors": {
+    "": {
+      "message": "should be an object with an integer property foo only",
+      "ref": undefined,
+      "type": "additionalProperties",
+      "types": {
+        "additionalProperties": "should be an object with an integer property foo only",
+      },
+    },
+    "foo": {
+      "message": "should be an object with an integer property foo only",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "type",
+      "types": {
+        "type": "should be an object with an integer property foo only",
+      },
+    },
+  },
+  "values": {},
+}
+`;
+
+exports[`ajvResolver with errorMessage > should return the same customized message when some properties are undefined 1`] = `
+{
+  "errors": {
+    "foo": {
+      "message": "should be an object with an integer property foo only",
+      "ref": {
+        "name": "foo",
+      },
+      "type": "required",
+      "types": {
+        "required": "should be an object with an integer property foo only",
+      },
+    },
+  },
+  "values": {},
+}
+`;
diff --git a/ajv/src/__tests__/__snapshots__/ajv.ts.snap b/ajv/src/__tests__/__snapshots__/ajv.ts.snap
index d96b1b5..a113c0e 100644
--- a/ajv/src/__tests__/__snapshots__/ajv.ts.snap
+++ b/ajv/src/__tests__/__snapshots__/ajv.ts.snap
@@ -104,9 +104,9 @@ exports[`ajvResolver > should return all the error messages from ajvResolver whe
       "ref": {
         "name": "password",
       },
-      "type": "errorMessage",
+      "type": "pattern",
       "types": {
-        "errorMessage": "One uppercase character",
+        "pattern": "One uppercase character",
       },
     },
     "username": {
@@ -152,9 +152,9 @@ exports[`ajvResolver > should return all the error messages from ajvResolver whe
       "ref": {
         "name": "password",
       },
-      "type": "errorMessage",
+      "type": "pattern",
       "types": {
-        "errorMessage": "One uppercase character",
+        "pattern": "One uppercase character",
       },
     },
     "username": {
@@ -194,7 +194,7 @@ exports[`ajvResolver > should return single error message from ajvResolver when
       "ref": {
         "name": "password",
       },
-      "type": "errorMessage",
+      "type": "pattern",
     },
     "username": {
       "message": "must NOT have fewer than 3 characters",
@@ -230,7 +230,7 @@ exports[`ajvResolver > should return single error message from ajvResolver when
       "ref": {
         "name": "password",
       },
-      "type": "errorMessage",
+      "type": "pattern",
     },
     "username": {
       "message": "must NOT have fewer than 3 characters",
diff --git a/ajv/src/__tests__/ajv-errors.ts b/ajv/src/__tests__/ajv-errors.ts
new file mode 100644
index 0000000..63726ce
--- /dev/null
+++ b/ajv/src/__tests__/ajv-errors.ts
@@ -0,0 +1,227 @@
+import { ajvResolver } from '..';
+import * as fixture from './__fixtures__/data-errors';
+
+const shouldUseNativeValidation = false;
+
+describe('ajvResolver with errorMessage', () => {
+  it('should return values when validation pass', async () => {
+    expect(
+      await ajvResolver(fixture.schemaA)(fixture.validDataA, undefined, {
+        fields: fixture.fieldsA,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      }),
+    ).toEqual({
+      values: fixture.validDataA,
+      errors: {},
+    });
+  });
+
+  it('should return customized error messages when validation fails', async () => {
+    expect(
+      await ajvResolver(fixture.schemaA)(
+        fixture.invalidDataA,
+        {},
+        {
+          fields: fixture.fieldsA,
+          criteriaMode: 'all',
+          shouldUseNativeValidation,
+        },
+      ),
+    ).toMatchSnapshot();
+  });
+
+  it('should return customized error messages when requirement fails', async () => {
+    expect(
+      await ajvResolver(fixture.schemaA)({}, undefined, {
+        fields: fixture.fieldsA,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      }),
+    ).toMatchSnapshot();
+  });
+
+  it('should return customized error messages when some properties are undefined', async () => {
+    expect(
+      await ajvResolver(fixture.schemaA, undefined, { mode: 'sync' })(
+        fixture.undefinedDataA,
+        undefined,
+        {
+          fields: fixture.fieldsA,
+          criteriaMode: 'all',
+          shouldUseNativeValidation,
+        },
+      ),
+    ).toMatchSnapshot();
+  });
+
+  it('should return the same customized message for all validation failures', async () => {
+    expect(
+      await ajvResolver(fixture.schemaB)(
+        fixture.invalidDataB,
+        {},
+        {
+          fields: fixture.fieldsRest,
+          criteriaMode: 'all',
+          shouldUseNativeValidation,
+        },
+      ),
+    ).toMatchSnapshot();
+  });
+
+  it('should return the same customized error message when requirement fails', async () => {
+    expect(
+      await ajvResolver(fixture.schemaB)({}, undefined, {
+        fields: fixture.fieldsRest,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      }),
+    ).toMatchSnapshot();
+  });
+
+  it('should return the same customized message when some properties are undefined', async () => {
+    expect(
+      await ajvResolver(fixture.schemaB)(fixture.undefinedDataB, undefined, {
+        fields: fixture.fieldsRest,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      }),
+    ).toMatchSnapshot();
+  });
+
+  it('should return customized error messages for certain keywords when walidation fails', async () => {
+    expect(
+      await ajvResolver(fixture.schemaC)(
+        fixture.invalidDataC,
+        {},
+        {
+          fields: fixture.fieldsRest,
+          criteriaMode: 'all',
+          shouldUseNativeValidation,
+        },
+      ),
+    ).toMatchSnapshot();
+  });
+
+  it('should return customized error messages for certain keywords when requirement fails', async () => {
+    expect(
+      await ajvResolver(fixture.schemaC)({}, undefined, {
+        fields: fixture.fieldsRest,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      }),
+    ).toMatchSnapshot();
+  });
+
+  it('should return customized error messages for certain keywords when some properties are undefined', async () => {
+    expect(
+      await ajvResolver(fixture.schemaC)(fixture.undefinedDataC, undefined, {
+        fields: fixture.fieldsRest,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      }),
+    ).toMatchSnapshot();
+  });
+
+  it('should return different messages for different properties when walidation fails', async () => {
+    expect(
+      await ajvResolver(fixture.schemaD)(
+        fixture.invalidDataD,
+        {},
+        {
+          fields: fixture.fieldsRest,
+          criteriaMode: 'all',
+          shouldUseNativeValidation,
+        },
+      ),
+    ).toMatchSnapshot();
+  });
+
+  it('should return different messages for different properties when requirement fails', async () => {
+    expect(
+      await ajvResolver(fixture.schemaD)({}, undefined, {
+        fields: fixture.fieldsRest,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      }),
+    ).toMatchSnapshot();
+  });
+
+  it('should return different messages for different properties when some properties are undefined', async () => {
+    expect(
+      await ajvResolver(fixture.schemaD)(fixture.undefinedDataD, undefined, {
+        fields: fixture.fieldsRest,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      }),
+    ).toMatchSnapshot();
+  });
+
+  it('should return customized errors for properties/items when walidation fails', async () => {
+    expect(
+      await ajvResolver(fixture.schemaE)(
+        fixture.invalidDataE,
+        {},
+        {
+          fields: fixture.fieldsRest,
+          criteriaMode: 'all',
+          shouldUseNativeValidation,
+        },
+      ),
+    ).toMatchSnapshot();
+  });
+
+  it('should return customized errors for properties/items when requirement fails', async () => {
+    expect(
+      await ajvResolver(fixture.schemaE)({}, undefined, {
+        fields: fixture.fieldsRest,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      }),
+    ).toMatchSnapshot();
+  });
+
+  it('should return customized errors for properties/items when some properties are undefined', async () => {
+    expect(
+      await ajvResolver(fixture.schemaE)(fixture.undefinedDataE, undefined, {
+        fields: fixture.fieldsRest,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      }),
+    ).toMatchSnapshot();
+  });
+
+  it('should return a default message if there is no specific message for the error when walidation fails', async () => {
+    expect(
+      await ajvResolver(fixture.schemaF)(
+        fixture.invalidDataF,
+        {},
+        {
+          fields: fixture.fieldsRest,
+          criteriaMode: 'all',
+          shouldUseNativeValidation,
+        },
+      ),
+    ).toMatchSnapshot();
+  });
+
+  it('should return a default message if there is no specific message for the error when requirement fails', async () => {
+    expect(
+      await ajvResolver(fixture.schemaF)({}, undefined, {
+        fields: fixture.fieldsRest,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      }),
+    ).toMatchSnapshot();
+  });
+
+  it('should return a default message if there is no specific message for the error when some properties are undefined', async () => {
+    expect(
+      await ajvResolver(fixture.schemaF)(fixture.undefinedDataF, undefined, {
+        fields: fixture.fieldsRest,
+        criteriaMode: 'all',
+        shouldUseNativeValidation,
+      }),
+    ).toMatchSnapshot();
+  });
+});
diff --git a/src/__tests__/toNestErrors.ts b/src/__tests__/toNestErrors.ts
index 983fdde..ebea917 100644
--- a/src/__tests__/toNestErrors.ts
+++ b/src/__tests__/toNestErrors.ts
@@ -39,6 +39,35 @@ test('transforms flat object to nested object and shouldUseNativeValidation: tru
   ).toHaveBeenCalledWith(flatObject.name.message);
 });
 
+test('transforms flat object to nested object with names option', () => {
+  const result = toNestErrors(
+    {
+      username: {
+        type: 'custom',
+        message: 'error',
+      },
+    },
+    {
+      names: ['username', 'username.first'],
+      fields: {
+        username: {
+          name: 'username',
+          ref: { name: 'username' },
+        },
+      },
+      shouldUseNativeValidation: false,
+    },
+  );
+
+  expect(result).toEqual({
+    username: {
+      type: 'custom',
+      message: 'error',
+      ref: { name: 'username' },
+    },
+  });
+});
+
 test('transforms flat object to nested object with root error for field array', () => {
   const result = toNestErrors(
     {