Repository: logaretm/vee-validate (ts)

Issue:
Validating with Zod not applying the `.transform()` method for `useField()`
### What happened?

I'm using the `.transform()` method to edit the data to be sent to the form submit function.
It works well for fields declared with `defineField()` method but not for custom inputs / fields declared with `useField()` method.

In the second case, the value is submitted without being transformed, even though it is being validated. Neither the `transform()` function nor the zod string transformations ( like `toUppoerCase()` ) work.

![image](https://github.com/logaretm/vee-validate/assets/103422781/ef554e75-bfa0-4195-af5d-66317e13d8a4)

### Reproduction steps

1. Create a custom input component using `useField()` with a Zod validation schema
2. Use it in a form context
3. Type a valid value and submit
4. The value is correctly validated but not transformed

!!! I have note tested for Safari

### Version

Vue.js 3.x and vee-validate 4.x

### What browsers are you seeing the problem on?

- [X] Firefox
- [X] Chrome
- [ ] Safari
- [X] Microsoft Edge

### Relevant log output

_No response_

### Demo link

https://stackblitz.com/edit/vitejs-vite-mrsjdx?file=src%2FApp.vue,src%2Fcomponents%2FForm1.vue,src%2Fcomponents%2FForm2.vue,src%2Fcomponents%2FInputText.vue&terminal=dev

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](CONDUCT.md)

Pull request:
fix: ensure transformed values in field-level schemas are used on submit
🔎 __Overview__

This PR allows for the transformed value to pass through as the submitted value, when the transformation is declared in a field-level schema in `useField` (as opposed to a top-level schema declared in `useForm`).  This affects both Zod and Yup.

This is a revision of https://github.com/logaretm/vee-validate/pull/4692 

- ✅ Many internal types adjusted to allow for a `<TInput, TOutput>` vs `<TValues>`
- ✅ Typecheck passes
- ✅ Rebased
- ✅ All existing tests pass
- ✅ New test with zod and a new test with yup added for this behavior

🤓 __Code snippets/examples (if applicable)__

```ts
// in a field component

const testRules = toTypedSchema(z.string().transform(value => value.trim()));
const { value } = useField('test', testRules);

// in a form component / page

const { handleSubmit } = useForm<{ test: string }>();

handleSubmit((values) => {
  console.log(values.test); // logs "hello" (with this PR)
});
```

With the above field-level usage, on submission of the value `"  hello  "` (notice the spaces) with the `main` branch, the value in `values.test` is `"  hello  "`. Notice the transformation `.trim` has not been applied.

See the bundled tests for more detail.

✔ __Issues affected__

Closes #4713 
 


Patch:
diff --git a/packages/rules/src/toTypedSchema.ts b/packages/rules/src/toTypedSchema.ts
index a5b47d88..d5ef2486 100644
--- a/packages/rules/src/toTypedSchema.ts
+++ b/packages/rules/src/toTypedSchema.ts
@@ -1,5 +1,5 @@
 import { keysOf } from '../../vee-validate/src/utils';
-import { TypedSchema, RawFormSchema, validateObject, TypedSchemaError, validate } from 'vee-validate';
+import { TypedSchema, RawFormSchema, validateObject, TypedSchemaError, validate, GenericObject } from 'vee-validate';
 import { Optional, isObject } from '../../shared';
 
 export function toTypedSchema<TOutput = any, TInput extends Optional<TOutput> = Optional<TOutput>>(
@@ -21,7 +21,7 @@ export function toTypedSchema<TOutput = any, TInput extends Optional<TOutput> =
         };
       }
 
-      const result = await validateObject<TInput, TOutput>(rawSchema, values);
+      const result = await validateObject(rawSchema, values as GenericObject | undefined);
 
       return {
         errors: keysOf(result.errors).map(path => {
diff --git a/packages/vee-validate/src/types/forms.ts b/packages/vee-validate/src/types/forms.ts
index 612ad71c..07287629 100644
--- a/packages/vee-validate/src/types/forms.ts
+++ b/packages/vee-validate/src/types/forms.ts
@@ -4,11 +4,16 @@ import { FieldValidationMetaInfo } from '../../../shared';
 import { Path, PathValue } from './paths';
 import { PartialDeep } from 'type-fest';
 
-export interface ValidationResult {
+export interface ValidationResult<TValue = unknown> {
   errors: string[];
   valid: boolean;
+  value?: TValue;
 }
 
+export type FlattenAndMapPathsValidationResult<TInput extends GenericObject, TOutput extends GenericObject> = {
+  [K in Path<TInput>]: ValidationResult<TOutput[K]>;
+};
+
 export interface TypedSchemaError {
   path?: string;
   errors: string[];
@@ -81,17 +86,17 @@ export interface ValidationOptions {
   warn: boolean;
 }
 
-export type FieldValidator = (opts?: Partial<ValidationOptions>) => Promise<ValidationResult>;
+export type FieldValidator<TOutput> = (opts?: Partial<ValidationOptions>) => Promise<ValidationResult<TOutput>>;
 
-export interface PathStateConfig {
+export interface PathStateConfig<TOutput> {
   bails: boolean;
   label: MaybeRefOrGetter<string | undefined>;
   type: InputType;
-  validate: FieldValidator;
+  validate: FieldValidator<TOutput>;
   schema?: MaybeRefOrGetter<TypedSchema | undefined>;
 }
 
-export interface PathState<TValue = unknown> {
+export interface PathState<TInput = unknown, TOutput = TInput> {
   id: number | number[];
   path: string;
   touched: boolean;
@@ -100,8 +105,8 @@ export interface PathState<TValue = unknown> {
   required: boolean;
   validated: boolean;
   pending: boolean;
-  initialValue: TValue | undefined;
-  value: TValue | undefined;
+  initialValue: TInput | undefined;
+  value: TInput | undefined;
   errors: string[];
   bails: boolean;
   label: string | undefined;
@@ -112,7 +117,7 @@ export interface PathState<TValue = unknown> {
     pendingUnmount: Record<string, boolean>;
     pendingReset: boolean;
   };
-  validate?: FieldValidator;
+  validate?: FieldValidator<TOutput>;
 }
 
 export interface FieldEntry<TValue = unknown> {
@@ -139,29 +144,29 @@ export interface PrivateFieldArrayContext<TValue = unknown> extends FieldArrayCo
   path: MaybeRefOrGetter<string>;
 }
 
-export interface PrivateFieldContext<TValue = unknown> {
+export interface PrivateFieldContext<TInput = unknown, TOutput = TInput> {
   id: number;
   name: MaybeRef<string>;
-  value: Ref<TValue>;
-  meta: FieldMeta<TValue>;
+  value: Ref<TInput>;
+  meta: FieldMeta<TInput>;
   errors: Ref<string[]>;
   errorMessage: Ref<string | undefined>;
   label?: MaybeRefOrGetter<string | undefined>;
   type?: string;
   bails?: boolean;
   keepValueOnUnmount?: MaybeRefOrGetter<boolean | undefined>;
-  checkedValue?: MaybeRefOrGetter<TValue>;
-  uncheckedValue?: MaybeRefOrGetter<TValue>;
+  checkedValue?: MaybeRefOrGetter<TInput>;
+  uncheckedValue?: MaybeRefOrGetter<TInput>;
   checked?: Ref<boolean>;
-  resetField(state?: Partial<FieldState<TValue>>): void;
+  resetField(state?: Partial<FieldState<TInput>>): void;
   handleReset(): void;
-  validate: FieldValidator;
+  validate: FieldValidator<TOutput>;
   handleChange(e: Event | unknown, shouldValidate?: boolean): void;
   handleBlur(e?: Event, shouldValidate?: boolean): void;
-  setState(state: Partial<FieldState<TValue>>): void;
+  setState(state: Partial<FieldState<TInput>>): void;
   setTouched(isTouched: boolean): void;
   setErrors(message: string | string[]): void;
-  setValue(value: TValue, shouldValidate?: boolean): void;
+  setValue(value: TInput, shouldValidate?: boolean): void;
 }
 
 export type FieldContext<TValue = unknown> = Omit<PrivateFieldContext<TValue>, 'id' | 'instances'>;
@@ -197,33 +202,38 @@ export interface FormActions<TValues extends GenericObject, TOutput = TValues> {
   resetField(field: Path<TValues>, state?: Partial<FieldState>): void;
 }
 
-export interface FormValidationResult<TValues, TOutput = TValues> {
+export interface FormValidationResult<TInput extends GenericObject, TOutput extends GenericObject = TInput> {
   valid: boolean;
-  results: Partial<Record<Path<TValues>, ValidationResult>>;
-  errors: Partial<Record<Path<TValues>, string>>;
-  values?: TOutput;
+  results: Partial<FlattenAndMapPathsValidationResult<TInput, TOutput>>;
+  errors: Partial<Record<Path<TInput>, string>>;
+  values?: Partial<TOutput>;
+  source: 'schema' | 'fields' | 'none';
 }
 
-export interface SubmissionContext<TValues extends GenericObject = GenericObject> extends FormActions<TValues> {
+export interface SubmissionContext<TInput extends GenericObject = GenericObject> extends FormActions<TInput> {
   evt?: Event;
-  controlledValues: Partial<TValues>;
+  controlledValues: Partial<TInput>;
 }
 
-export type SubmissionHandler<TValues extends GenericObject = GenericObject, TOutput = TValues, TReturn = unknown> = (
+export type SubmissionHandler<TInput extends GenericObject = GenericObject, TOutput = TInput, TReturn = unknown> = (
   values: TOutput,
-  ctx: SubmissionContext<TValues>,
+  ctx: SubmissionContext<TInput>,
 ) => TReturn;
 
-export interface InvalidSubmissionContext<TValues extends GenericObject = GenericObject> {
-  values: TValues;
+export interface InvalidSubmissionContext<
+  TInput extends GenericObject = GenericObject,
+  TOutput extends GenericObject = TInput,
+> {
+  values: TInput;
   evt?: Event;
-  errors: Partial<Record<Path<TValues>, string>>;
-  results: Partial<Record<Path<TValues>, ValidationResult>>;
+  errors: Partial<Record<Path<TInput>, string>>;
+  results: FormValidationResult<TInput, TOutput>['results'];
 }
 
-export type InvalidSubmissionHandler<TValues extends GenericObject = GenericObject> = (
-  ctx: InvalidSubmissionContext<TValues>,
-) => void;
+export type InvalidSubmissionHandler<
+  TInput extends GenericObject = GenericObject,
+  TOutput extends GenericObject = TInput,
+> = (ctx: InvalidSubmissionContext<TInput, TOutput>) => void;
 
 export type RawFormSchema<TValues> = Record<Path<TValues>, string | GenericValidateFunction | GenericObject>;
 
@@ -231,9 +241,9 @@ export type FieldPathLookup<TValues extends GenericObject = GenericObject> = Par
   Record<Path<TValues>, PrivateFieldContext | PrivateFieldContext[]>
 >;
 
-type HandleSubmitFactory<TValues extends GenericObject, TOutput = TValues> = <TReturn = unknown>(
+type HandleSubmitFactory<TValues extends GenericObject, TOutput extends GenericObject = TValues> = <TReturn = unknown>(
   cb: SubmissionHandler<TValues, TOutput, TReturn>,
-  onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues>,
+  onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues, TOutput>,
 ) => (e?: Event) => Promise<TReturn | undefined>;
 
 export type PublicPathState<TValue = unknown> = Omit<
@@ -310,8 +320,10 @@ export interface BaseInputBinds<TValue = unknown> {
   onInput: (e: Event) => void;
 }
 
-export interface PrivateFormContext<TValues extends GenericObject = GenericObject, TOutput = TValues>
-  extends FormActions<TValues> {
+export interface PrivateFormContext<
+  TValues extends GenericObject = GenericObject,
+  TOutput extends GenericObject = TValues,
+> extends FormActions<TValues> {
   formId: number;
   values: TValues;
   initialValues: Ref<Partial<TValues>>;
@@ -327,14 +339,17 @@ export interface PrivateFormContext<TValues extends GenericObject = GenericObjec
   keepValuesOnUnmount: MaybeRef<boolean>;
   validateSchema?: (mode: SchemaValidationMode) => Promise<FormValidationResult<TValues, TOutput>>;
   validate(opts?: Partial<ValidationOptions>): Promise<FormValidationResult<TValues, TOutput>>;
-  validateField(field: Path<TValues>, opts?: Partial<ValidationOptions>): Promise<ValidationResult>;
+  validateField<TPath extends Path<TValues>>(
+    field: TPath,
+    opts?: Partial<ValidationOptions>,
+  ): Promise<ValidationResult<TOutput[TPath]>>;
   stageInitialValue(path: string, value: unknown, updateOriginal?: boolean): void;
   unsetInitialValue(path: string): void;
   handleSubmit: HandleSubmitFactory<TValues, TOutput> & { withControlled: HandleSubmitFactory<TValues, TOutput> };
   setFieldInitialValue(path: string, value: unknown, updateOriginal?: boolean): void;
   createPathState<TPath extends Path<TValues>>(
     path: MaybeRef<TPath>,
-    config?: Partial<PathStateConfig>,
+    config?: Partial<PathStateConfig<TOutput[TPath]>>,
   ): PathState<PathValue<TValues, TPath>>;
   getPathState<TPath extends Path<TValues>>(path: TPath): PathState<PathValue<TValues, TPath>> | undefined;
   getAllPathStates(): PathState[];
@@ -387,7 +402,7 @@ export interface PrivateFormContext<TValues extends GenericObject = GenericObjec
   ): Ref<BaseInputBinds<TValue> & TExtras>;
 }
 
-export interface FormContext<TValues extends GenericObject = GenericObject, TOutput = TValues>
+export interface FormContext<TValues extends GenericObject = GenericObject, TOutput extends GenericObject = TValues>
   extends Omit<
     PrivateFormContext<TValues, TOutput>,
     | 'formId'
diff --git a/packages/vee-validate/src/useFieldState.ts b/packages/vee-validate/src/useFieldState.ts
index bf9dac06..b31de274 100644
--- a/packages/vee-validate/src/useFieldState.ts
+++ b/packages/vee-validate/src/useFieldState.ts
@@ -18,14 +18,14 @@ export interface FieldStateComposable<TValue = unknown> {
   setState(state: Partial<StateSetterInit<TValue>>): void;
 }
 
-export interface StateInit<TValue = unknown> {
-  modelValue: MaybeRef<TValue>;
+export interface StateInit<TInput = unknown, TOutput = TInput> {
+  modelValue: MaybeRef<TInput>;
   form?: PrivateFormContext;
   bails: boolean;
   label?: MaybeRefOrGetter<string | undefined>;
   type?: InputType;
-  validate?: FieldValidator;
-  schema?: MaybeRefOrGetter<TypedSchema<TValue> | undefined>;
+  validate?: FieldValidator<TOutput>;
+  schema?: MaybeRefOrGetter<TypedSchema<TInput> | undefined>;
 }
 
 let ID_COUNTER = 0;
diff --git a/packages/vee-validate/src/useForm.ts b/packages/vee-validate/src/useForm.ts
index b8560f98..9d1c18ce 100644
--- a/packages/vee-validate/src/useForm.ts
+++ b/packages/vee-validate/src/useForm.ts
@@ -26,6 +26,7 @@ import {
   ValidationResult,
   FormState,
   FormValidationResult,
+  FlattenAndMapPathsValidationResult,
   PrivateFormContext,
   FormContext,
   FormErrors,
@@ -109,7 +110,7 @@ function resolveInitialValues<TValues extends GenericObject = GenericObject>(opt
 
 export function useForm<
   TValues extends GenericObject = GenericObject,
-  TOutput = TValues,
+  TOutput extends GenericObject = TValues,
   TSchema extends FormSchema<TValues> | TypedSchema<TValues, TOutput> =
     | FormSchema<TValues>
     | TypedSchema<TValues, TOutput>,
@@ -266,10 +267,10 @@ export function useForm<
 
   const schema = opts?.validationSchema;
 
-  function createPathState<TValue>(
-    path: MaybeRefOrGetter<Path<TValues>>,
-    config?: Partial<PathStateConfig>,
-  ): PathState<TValue> {
+  function createPathState<TPath extends Path<TValues>>(
+    path: MaybeRefOrGetter<TPath>,
+    config?: Partial<PathStateConfig<TOutput[TPath]>>,
+  ): PathState<TValues[TPath], TOutput[TPath]> {
     const initialValue = computed(() => getFromPath(initialValues.value, toValue(path)));
     const pathStateExists = pathStateLookup.value[toValue(path)];
     const isCheckboxOrRadio = config?.type === 'checkbox' || config?.type === 'radio';
@@ -285,7 +286,7 @@ export function useForm<
       pathStateExists.fieldsCount++;
       pathStateExists.__flags.pendingUnmount[id] = false;
 
-      return pathStateExists as PathState<TValue>;
+      return pathStateExists as PathState<TValues[TPath], TOutput[TPath]>;
     }
 
     const currentValue = computed(() => getFromPath(formValues, toValue(path)));
@@ -336,7 +337,7 @@ export function useForm<
       dirty: computed(() => {
         return !isEqual(unref(currentValue), unref(initialValue));
       }),
-    }) as PathState<TValue>;
+    }) as PathState<TValues[TPath], TOutput[TPath]>;
 
     pathStates.value.push(state);
     pathStateLookup.value[pathValue] = state;
@@ -432,11 +433,17 @@ export function useForm<
 
           return validation;
         },
-        { valid: formResult.valid, results: {}, errors: {} } as FormValidationResult<TValues>,
+        {
+          valid: formResult.valid,
+          results: {},
+          errors: {},
+          source: formResult.source,
+        } as FormValidationResult<TValues>,
       );
 
       if (formResult.values) {
         results.values = formResult.values;
+        results.source = formResult.source;
       }
 
       keysOf(results.results).forEach(path => {
@@ -510,7 +517,7 @@ export function useForm<
   function makeSubmissionFactory(onlyControlled: boolean) {
     return function submitHandlerFactory<TReturn = unknown>(
       fn?: SubmissionHandler<TValues, TOutput, TReturn>,
-      onValidationError?: InvalidSubmissionHandler<TValues>,
+      onValidationError?: InvalidSubmissionHandler<TValues, TOutput>,
     ) {
       return function submissionHandler(e: unknown) {
         if (e instanceof Event) {
@@ -530,8 +537,12 @@ export function useForm<
             if (result.valid && typeof fn === 'function') {
               const controlled = deepCopy(controlledValues.value);
               let submittedValues = (onlyControlled ? controlled : values) as unknown as TOutput;
+
               if (result.values) {
-                submittedValues = result.values;
+                submittedValues =
+                  result.source === 'schema'
+                    ? (result.values as TOutput)
+                    : Object.assign({}, submittedValues, result.values);
               }
 
               return fn(submittedValues, {
@@ -859,14 +870,16 @@ export function useForm<
             key: state.path,
             valid: true,
             errors: [],
+            value: undefined,
           });
         }
 
-        return state.validate(opts).then((result: ValidationResult) => {
+        return state.validate(opts).then(result => {
           return {
             key: state.path,
             valid: result.valid,
             errors: result.errors,
+            value: result.value,
           };
         });
       }),
@@ -874,14 +887,20 @@ export function useForm<
 
     isValidating.value = false;
 
-    const results: Partial<FlattenAndSetPathsType<TValues, ValidationResult>> = {};
+    const results: Partial<FlattenAndMapPathsValidationResult<TValues, TOutput>> = {};
     const errors: Partial<FlattenAndSetPathsType<TValues, string>> = {};
+    const values: Partial<TOutput> = {};
+
     for (const validation of validations) {
       results[validation.key as Path<TValues>] = {
         valid: validation.valid,
         errors: validation.errors,
       };
 
+      if (validation.value) {
+        setInPath(values, validation.key, validation.value);
+      }
+
       if (validation.errors.length) {
         errors[validation.key as Path<TValues>] = validation.errors[0];
       }
@@ -891,10 +910,15 @@ export function useForm<
       valid: validations.every(r => r.valid),
       results,
       errors,
+      values,
+      source: 'fields',
     };
   }
 
-  async function validateField(path: Path<TValues>, opts?: Partial<ValidationOptions>): Promise<ValidationResult> {
+  async function validateField<TPath extends Path<TValues>>(
+    path: TPath,
+    opts?: Partial<ValidationOptions>,
+  ): Promise<ValidationResult<TOutput[TPath]>> {
     const state = findPathState(path);
     if (state && opts?.mode !== 'silent') {
       state.validated = true;
@@ -945,7 +969,7 @@ export function useForm<
   async function _validateSchema(): Promise<FormValidationResult<TValues, TOutput>> {
     const schemaValue = unref(schema);
     if (!schemaValue) {
-      return { valid: true, results: {}, errors: {} };
+      return { valid: true, results: {}, errors: {}, source: 'none' };
     }
 
     isValidating.value = true;
@@ -1291,7 +1315,10 @@ function useFormInitialValues<TValues extends GenericObject>(
   };
 }
 
-function mergeValidationResults(a: ValidationResult, b?: ValidationResult): ValidationResult {
+function mergeValidationResults<TValue extends GenericObject>(
+  a: ValidationResult<TValue>,
+  b?: ValidationResult<TValue>,
+): ValidationResult<TValue> {
   if (!b) {
     return a;
   }
diff --git a/packages/vee-validate/src/useValidateField.ts b/packages/vee-validate/src/useValidateField.ts
index f6a79992..7763516e 100644
--- a/packages/vee-validate/src/useValidateField.ts
+++ b/packages/vee-validate/src/useValidateField.ts
@@ -6,13 +6,13 @@ import { injectWithSelf, warn } from './utils';
 /**
  * Validates a single field
  */
-export function useValidateField(path?: MaybeRefOrGetter<string>) {
+export function useValidateField<TOutput>(path?: MaybeRefOrGetter<string>) {
   const form = injectWithSelf(FormContextKey);
   const field = path ? undefined : inject(FieldContextKey);
 
-  return function validateField(): Promise<ValidationResult> {
+  return function validateField(): Promise<ValidationResult<TOutput>> {
     if (field) {
-      return field.validate();
+      return field.validate() as Promise<ValidationResult<TOutput>>;
     }
 
     if (form && path) {
diff --git a/packages/vee-validate/src/useValidateForm.ts b/packages/vee-validate/src/useValidateForm.ts
index eb532ffe..32410330 100644
--- a/packages/vee-validate/src/useValidateForm.ts
+++ b/packages/vee-validate/src/useValidateForm.ts
@@ -15,7 +15,7 @@ export function useValidateForm<TValues extends Record<string, unknown> = Record
 
   return function validateField(): Promise<FormValidationResult<TValues>> {
     if (!form) {
-      return Promise.resolve({ results: {}, errors: {}, valid: true });
+      return Promise.resolve({ results: {}, errors: {}, valid: true, source: 'none' });
     }
 
     return form.validate();
diff --git a/packages/vee-validate/src/validate.ts b/packages/vee-validate/src/validate.ts
index 2c29ad53..4bda8976 100644
--- a/packages/vee-validate/src/validate.ts
+++ b/packages/vee-validate/src/validate.ts
@@ -6,9 +6,11 @@ import {
   ValidationResult,
   GenericValidateFunction,
   TypedSchema,
+  FlattenAndMapPathsValidationResult,
   FormValidationResult,
   RawFormSchema,
   YupSchema,
+  GenericObject,
   TypedSchemaError,
   Path,
   TypedSchemaContext,
@@ -18,13 +20,13 @@ import { isCallable, FieldValidationMetaInfo } from '../../shared';
 /**
  * Used internally
  */
-interface FieldValidationContext<TValue = unknown> {
+interface FieldValidationContext<TInput = unknown, TOutput = TInput> {
   name: string;
   label?: string;
   rules:
-    | GenericValidateFunction<TValue>
-    | GenericValidateFunction<TValue>[]
-    | TypedSchema<TValue>
+    | GenericValidateFunction<TInput>
+    | GenericValidateFunction<TInput>[]
+    | TypedSchema<TInput, TOutput>
     | string
     | Record<string, unknown>;
   bails: boolean;
@@ -41,18 +43,18 @@ interface ValidationOptions {
 /**
  * Validates a value against the rules.
  */
-export async function validate<TValue = unknown>(
-  value: TValue,
+export async function validate<TInput, TOutput>(
+  value: TInput,
   rules:
     | string
     | Record<string, unknown | unknown[]>
-    | GenericValidateFunction<TValue>
-    | GenericValidateFunction<TValue>[]
-    | TypedSchema<TValue>,
+    | GenericValidateFunction<TInput>
+    | GenericValidateFunction<TInput>[]
+    | TypedSchema<TInput, TOutput>,
   options: ValidationOptions = {},
-): Promise<ValidationResult> {
+): Promise<ValidationResult<TOutput>> {
   const shouldBail = options?.bails;
-  const field: FieldValidationContext<TValue> = {
+  const field: FieldValidationContext<TInput, TOutput> = {
     name: options?.name || '{field}',
     rules,
     label: options?.label,
@@ -61,18 +63,20 @@ export async function validate<TValue = unknown>(
   };
 
   const result = await _validate(field, value);
-  const errors = result.errors;
 
   return {
-    errors,
-    valid: !errors.length,
+    ...result,
+    valid: !result.errors.length,
   };
 }
 
 /**
  * Starts the validation process.
  */
-async function _validate<TValue = unknown>(field: FieldValidationContext<TValue>, value: TValue) {
+async function _validate<TInput = unknown, TOutput = TInput>(
+  field: FieldValidationContext<TInput, TOutput>,
+  value: TInput,
+) {
   const rules = field.rules;
   if (isTypedSchema(rules) || isYupValidator(rules)) {
     return validateFieldWithTypedSchema(value, { ...field, rules });
@@ -222,6 +226,7 @@ async function validateFieldWithTypedSchema(
   }
 
   return {
+    value: result.value,
     errors: messages,
   };
 }
@@ -300,14 +305,14 @@ function fillTargetValues(params: unknown[] | Record<string, unknown>, crossTabl
   );
 }
 
-export async function validateTypedSchema<TValues, TOutput = TValues>(
+export async function validateTypedSchema<TValues extends GenericObject, TOutput extends GenericObject = TValues>(
   schema: TypedSchema<TValues> | YupSchema<TValues>,
   values: TValues,
 ): Promise<FormValidationResult<TValues, TOutput>> {
   const typedSchema = isTypedSchema(schema) ? schema : yupToTypedSchema(schema);
   const validationResult = await typedSchema.parse(deepCopy(values));
 
-  const results: Partial<Record<Path<TValues>, ValidationResult>> = {};
+  const results: Partial<FlattenAndMapPathsValidationResult<TValues, TOutput>> = {};
   const errors: Partial<Record<Path<TValues>, string>> = {};
   for (const error of validationResult.errors) {
     const messages = error.errors;
@@ -327,12 +332,13 @@ export async function validateTypedSchema<TValues, TOutput = TValues>(
     results,
     errors,
     values: validationResult.value,
+    source: 'schema',
   };
 }
 
-export async function validateObjectSchema<TValues, TOutput>(
+export async function validateObjectSchema<TValues extends GenericObject, TOutput extends GenericObject>(
   schema: RawFormSchema<TValues>,
-  values: TValues,
+  values: TValues | undefined,
   opts?: Partial<{ names: Record<string, { name: string; label: string }>; bailsMap: Record<string, boolean> }>,
 ): Promise<FormValidationResult<TValues, TOutput>> {
   const paths = keysOf(schema) as Path<TValues>[];
@@ -354,7 +360,7 @@ export async function validateObjectSchema<TValues, TOutput>(
   let isAllValid = true;
   const validationResults = await Promise.all(validations);
 
-  const results: Partial<Record<Path<TValues>, ValidationResult>> = {};
+  const results: Partial<FlattenAndMapPathsValidationResult<TValues, TOutput>> = {};
   const errors: Partial<Record<Path<TValues>, string>> = {};
   for (const result of validationResults) {
     results[result.path] = {
@@ -372,5 +378,6 @@ export async function validateObjectSchema<TValues, TOutput>(
     valid: isAllValid,
     results,
     errors,
+    source: 'schema',
   };
 }


Test patch:
diff --git a/packages/vee-validate/tests/useForm.spec.ts b/packages/vee-validate/tests/useForm.spec.ts
index ba517372..f1a2ce4f 100644
--- a/packages/vee-validate/tests/useForm.spec.ts
+++ b/packages/vee-validate/tests/useForm.spec.ts
@@ -226,6 +226,7 @@ describe('useForm()', () => {
     const result = await validate();
     expect(result).toEqual({
       valid: false,
+      source: 'fields',
       errors: {
         field1: REQUIRED_MESSAGE,
         field2: REQUIRED_MESSAGE,
@@ -240,6 +241,7 @@ describe('useForm()', () => {
           errors: [REQUIRED_MESSAGE],
         },
       },
+      values: {},
     });
   });
 
@@ -269,6 +271,7 @@ describe('useForm()', () => {
     const result = await pending;
     expect(result).toEqual({
       valid: false,
+      source: 'schema',
       errors: {
         field1: REQUIRED_MESSAGE,
         field2: REQUIRED_MESSAGE,
diff --git a/packages/yup/tests/yup.spec.ts b/packages/yup/tests/yup.spec.ts
index fa5a372e..4446d615 100644
--- a/packages/yup/tests/yup.spec.ts
+++ b/packages/yup/tests/yup.spec.ts
@@ -546,6 +546,47 @@ test('reports required state for field-level schemas without a form context', as
   );
 });
 
+test('uses transformed value as submitted value', async () => {
+  const onSubmitSpy = vi.fn();
+  let onSubmit!: () => void;
+
+  const wrapper = mountWithHoc({
+    setup() {
+      const { handleSubmit } = useForm<{
+        req: string;
+      }>();
+
+      const { value } = useField('test', toTypedSchema(yup.string().transform(val => `modified: ${val}`)));
+
+      // submit now
+      onSubmit = handleSubmit(onSubmitSpy);
+
+      return {
+        value,
+      };
+    },
+    template: `
+      <div>
+        <input v-model="value" type="text">
+      </div>
+    `,
+  });
+
+  const input = wrapper.$el.querySelector('input');
+
+  setValue(input, '12345678');
+  await flushPromises();
+  onSubmit();
+  await flushPromises();
+  await expect(onSubmitSpy).toHaveBeenCalledTimes(1);
+  await expect(onSubmitSpy).toHaveBeenLastCalledWith(
+    expect.objectContaining({
+      test: 'modified: 12345678',
+    }),
+    expect.anything(),
+  );
+});
+
 test('supports yup.strip', async () => {
   const onSubmitSpy = vi.fn();
   let onSubmit!: () => void;
diff --git a/packages/zod/tests/zod.spec.ts b/packages/zod/tests/zod.spec.ts
index a25342dd..b6d23888 100644
--- a/packages/zod/tests/zod.spec.ts
+++ b/packages/zod/tests/zod.spec.ts
@@ -660,3 +660,45 @@ test('reports required state for field-level schemas without a form context', as
     }),
   );
 });
+
+test('uses transformed value as submitted value', async () => {
+  const onSubmitSpy = vi.fn();
+  let onSubmit!: () => void;
+
+  const wrapper = mountWithHoc({
+    setup() {
+      const { handleSubmit } = useForm<{
+        test: string;
+      }>();
+
+      const testRules = toTypedSchema(z.string().transform(value => `modified: ${value}`));
+      const { value } = useField('test', testRules);
+
+      // submit now
+      onSubmit = handleSubmit(onSubmitSpy);
+
+      return {
+        value,
+      };
+    },
+    template: `
+      <div>
+          <input v-model="value" type="text">
+      </div>
+    `,
+  });
+
+  const input = wrapper.$el.querySelector('input');
+
+  setValue(input, '12345678');
+  await flushPromises();
+  onSubmit();
+  await flushPromises();
+  await expect(onSubmitSpy).toHaveBeenCalledTimes(1);
+  await expect(onSubmitSpy).toHaveBeenLastCalledWith(
+    expect.objectContaining({
+      test: 'modified: 12345678',
+    }),
+    expect.anything(),
+  );
+});