Repository: jquense/yup
Pull request #1545 (2022-01-03 12:57:34)

chore: improve internal test run APIs and Error handling


Patch:
diff --git a/CHANGELOG-pre.md b/CHANGELOG-pre.md
index 10c3a81..6e5f0d9 100644
--- a/CHANGELOG-pre.md
+++ b/CHANGELOG-pre.md
@@ -1,3 +1,27 @@
+# [1.0.0-beta.0](https://github.com/jquense/yup/compare/v1.0.0-alpha.4...v1.0.0-beta.0) (2021-12-29)
+
+
+* feat!: add json() method and remove default object/array coercion ([94b73c4](https://github.com/jquense/yup/commit/94b73c438b3d355253f488325e06c69378e71fc1))
+
+
+### Features
+
+* Make Array generic consistent with others ([a82353f](https://github.com/jquense/yup/commit/a82353f37735daec6e42d18bd4cc0efe52a20f50))
+
+
+### BREAKING CHANGES
+
+* types only, `ArraySchema`  initial generic is the array type not the type of the array element. `array<T>()` is still the inner type.
+* object and array schema no longer parse JSON strings by default, nor do they return `null` for invalid casts.
+
+```ts
+object().json().cast('{}')
+array().json().cast('[]')
+```
+to mimic the previous behavior
+
+
+
 # [1.0.0-alpha.4](https://github.com/jquense/yup/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2021-12-29)
 
 ### Bug Fixes
diff --git a/README.md b/README.md
index 21e1026..1980cce 100644
--- a/README.md
+++ b/README.md
@@ -1399,6 +1399,7 @@ and use the result as the limit.
 
 Define an array schema. Arrays can be typed or not, When specifying the element type, `cast` and `isValid`
 will apply to the elements as well. Options passed into `isValid` are passed also passed to child schemas.
+
 Inherits from [`Schema`](#Schema).
 
 ```js
@@ -1418,9 +1419,7 @@ array().of(yup.number());
 array(yup.number());
 ```
 
-The default `cast` behavior for `array` is: [`JSON.parse`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse)
-
-Failed casts return: `null`;
+Arrays have no default casting behavior.
 
 #### `array.of(type: Schema): this`
 
diff --git a/src/Lazy.ts b/src/Lazy.ts
index 4e79e14..a74bbea 100644
--- a/src/Lazy.ts
+++ b/src/Lazy.ts
@@ -1,5 +1,5 @@
 import isSchema from './util/isSchema';
-import type { AnyObject, Callback, ValidateOptions } from './types';
+import type { AnyObject, ValidateOptions } from './types';
 import type { ResolveOptions } from './Condition';
 
 import type {
@@ -82,13 +82,12 @@ class Lazy<T, TContext = AnyObject, TDefault = any, TFlags extends Flags = any>
     return this._resolve(value, options).cast(value, options);
   }
 
-  validate(
-    value: any,
-    options?: ValidateOptions,
-    maybeCb?: Callback,
-  ): Promise<T> {
-    // @ts-expect-error missing public callback on type
-    return this._resolve(value, options).validate(value, options, maybeCb);
+  asTest(value: any, options?: ValidateOptions<TContext>) {
+    return this._resolve(value, options).asTest(value, options);
+  }
+
+  validate(value: any, options?: ValidateOptions<TContext>): Promise<T> {
+    return this._resolve(value, options).validate(value, options);
   }
 
   validateSync(value: any, options?: ValidateOptions<TContext>): T {
diff --git a/src/array.ts b/src/array.ts
index 10b2e0d..fbe4bff 100644
--- a/src/array.ts
+++ b/src/array.ts
@@ -2,16 +2,13 @@ import isAbsent from './util/isAbsent';
 import isSchema from './util/isSchema';
 import printValue from './util/printValue';
 import { array as locale } from './locale';
-import runTests, { RunTest } from './util/runTests';
 import type {
   AnyObject,
   InternalOptions,
-  Callback,
   Message,
   Maybe,
   Optionals,
 } from './types';
-import ValidationError from './ValidationError';
 import type Reference from './Reference';
 import {
   Defined,
@@ -24,9 +21,14 @@ import {
   UnsetFlag,
   Concat,
 } from './util/types';
-import Schema, { SchemaInnerTypeDescription, SchemaSpec } from './schema';
+import Schema, {
+  RunTest,
+  SchemaInnerTypeDescription,
+  SchemaSpec,
+} from './schema';
 import { ResolveOptions } from './Condition';
 import parseJson from 'parse-json';
+import { ValidationError } from '.';
 
 type InnerType<T> = T extends Array<infer I> ? I : never;
 
@@ -36,9 +38,7 @@ export type RejectorFn = (
   array: readonly any[],
 ) => boolean;
 
-export function create<C extends AnyObject = AnyObject, T = any>(
-  type?: ISchema<T, C>,
-) {
+export function create<C = AnyObject, T = any>(type?: ISchema<T, C>) {
   return new ArraySchema<T[] | undefined, C>(type as any);
 }
 
@@ -89,28 +89,22 @@ export default class ArraySchema<
   protected _validate(
     _value: any,
     options: InternalOptions<TContext> = {},
-    callback: Callback,
+
+    panic: (err: Error, value: unknown) => void,
+    callback: (err: ValidationError[], value: unknown) => void,
   ) {
-    let errors = [] as ValidationError[];
-    let sync = options.sync;
-    let path = options.path;
+    // let sync = options.sync;
+    // let path = options.path;
     let innerType = this.innerType;
-    let endEarly = options.abortEarly ?? this.spec.abortEarly;
+    // let endEarly = options.abortEarly ?? this.spec.abortEarly;
     let recursive = options.recursive ?? this.spec.recursive;
 
     let originalValue =
       options.originalValue != null ? options.originalValue : _value;
 
-    super._validate(_value, options, (err, value) => {
-      if (err) {
-        if (!ValidationError.isError(err) || endEarly) {
-          return void callback(err, value);
-        }
-        errors.push(err);
-      }
-
+    super._validate(_value, options, panic, (arrayErrors, value) => {
       if (!recursive || !innerType || !this._typeCheck(value)) {
-        callback(errors[0] || null, value);
+        callback(arrayErrors, value);
         return;
       }
 
@@ -122,29 +116,24 @@ export default class ArraySchema<
         let item = value[idx];
         let path = `${options.path || ''}[${idx}]`;
 
-        // object._validate note for isStrict explanation
-        let innerOptions = {
+        tests[idx] = innerType!.asTest(item, {
           ...options,
           path,
-          strict: true,
           parent: value,
+          // FIXME
           index: idx,
           originalValue: originalValue[idx],
-        };
-
-        tests[idx] = (_, cb) => innerType!.validate(item, innerOptions, cb);
+        });
       }
 
-      runTests(
+      this.runTests(
         {
-          sync,
-          path,
           value,
-          errors,
-          endEarly,
           tests,
         },
-        callback,
+        panic,
+        (innerTypeErrors) =>
+          callback(innerTypeErrors.concat(arrayErrors), value),
       );
     });
   }
diff --git a/src/object.ts b/src/object.ts
index 894c98f..0ed8687 100644
--- a/src/object.ts
+++ b/src/object.ts
@@ -12,11 +12,9 @@ import {
 import { object as locale } from './locale';
 import sortFields from './util/sortFields';
 import sortByKeyOrder from './util/sortByKeyOrder';
-import runTests from './util/runTests';
-import { InternalOptions, Callback, Maybe, Message } from './types';
-import ValidationError from './ValidationError';
+import { InternalOptions, Maybe, Message } from './types';
 import type { Defined, Thunk, NotNull, _ } from './util/types';
-import type Reference from './Reference';
+import Reference from './Reference';
 import Schema, { SchemaObjectDescription, SchemaSpec } from './schema';
 import { ResolveOptions } from './Condition';
 import type {
@@ -30,6 +28,8 @@ import type {
   TypeFromShape,
 } from './util/objectTypes';
 import parseJson from './util/parseJson';
+import type { Test } from './util/createValidation';
+import type ValidationError from './ValidationError';
 
 export type { AnyObject };
 
@@ -205,14 +205,12 @@ export default class ObjectSchema<
   protected _validate(
     _value: any,
     opts: InternalOptions<TContext> = {},
-    callback: Callback,
+    panic: (err: Error, value: unknown) => void,
+    next: (err: ValidationError[], value: unknown) => void,
   ) {
-    let errors = [] as ValidationError[];
     let {
-      sync,
       from = [],
       originalValue = _value,
-      abortEarly = this.spec.abortEarly,
       recursive = this.spec.recursive,
     } = opts;
 
@@ -224,64 +222,41 @@ export default class ObjectSchema<
     opts.originalValue = originalValue;
     opts.from = from;
 
-    super._validate(_value, opts, (err, value) => {
-      if (err) {
-        if (!ValidationError.isError(err) || abortEarly) {
-          return void callback(err, value);
-        }
-        errors.push(err);
-      }
-
+    super._validate(_value, opts, panic, (objectErrors, value) => {
       if (!recursive || !isObject(value)) {
-        callback(errors[0] || null, value);
+        next(objectErrors, value);
         return;
       }
 
       originalValue = originalValue || value;
 
-      let tests = this._nodes.map((key) => (__: any, cb: Callback) => {
+      let tests = [] as Test[];
+      for (let key of this._nodes) {
+        let field = this.fields[key];
+
+        if (!field || Reference.isRef(field)) {
+          continue;
+        }
+
         let path =
           key.indexOf('.') === -1
             ? (opts.path ? `${opts.path}.` : '') + key
             : `${opts.path || ''}["${key}"]`;
 
-        let field = this.fields[key];
-
-        if (field && 'validate' in field) {
-          field.validate(
-            value[key],
-            {
-              ...opts,
-              // @ts-ignore
-              path,
-              from,
-              // inner fields are always strict:
-              // 1. this isn't strict so the casting will also have cast inner values
-              // 2. this is strict in which case the nested values weren't cast either
-              strict: true,
-              parent: value,
-              originalValue: originalValue[key],
-            },
-            cb,
-          );
-          return;
-        }
+        tests.push(
+          field.asTest(value[key], {
+            ...opts,
+            path,
+            from,
+            parent: value,
+            originalValue: originalValue[key],
+          }),
+        );
+      }
 
-        cb(null);
+      this.runTests({ tests, value }, panic, (fieldErrors) => {
+        next(fieldErrors.sort(this._sortErrors).concat(objectErrors), value);
       });
-
-      runTests(
-        {
-          sync,
-          tests,
-          value,
-          errors,
-          endEarly: abortEarly,
-          sort: this._sortErrors,
-          path: opts.path,
-        },
-        callback,
-      );
     });
   }
 
diff --git a/src/schema.ts b/src/schema.ts
index aa538f7..f028d83 100644
--- a/src/schema.ts
+++ b/src/schema.ts
@@ -7,11 +7,13 @@ import Condition, {
   ConditionConfig,
   ResolveOptions,
 } from './Condition';
-import runTests from './util/runTests';
 import createValidation, {
   TestFunction,
   Test,
   TestConfig,
+  NextCallback,
+  PanicCallback,
+  TestOptions,
 } from './util/createValidation';
 import printValue from './util/printValue';
 import Ref from './Reference';
@@ -20,7 +22,6 @@ import {
   ValidateOptions,
   TransformFunction,
   Message,
-  Callback,
   InternalOptions,
   Maybe,
   ExtraParams,
@@ -69,6 +70,18 @@ export interface CastOptions<C = {}> {
   path?: string;
 }
 
+export type RunTest = (
+  opts: TestOptions,
+  panic: PanicCallback,
+  next: NextCallback,
+) => void;
+
+export type TestRunOptions = {
+  tests: RunTest[];
+  args?: TestOptions;
+  value: any;
+};
+
 export interface SchemaRefDescription {
   type: 'ref';
   key: string;
@@ -265,7 +278,7 @@ export default abstract class Schema<
     // the deduping logic is consistent
     combined.withMutation((next) => {
       schema.tests.forEach((fn) => {
-        next.test(fn.OPTIONS);
+        next.test(fn.OPTIONS!);
       });
     });
 
@@ -353,7 +366,8 @@ export default abstract class Schema<
   protected _validate(
     _value: any,
     options: InternalOptions<TContext> = {},
-    cb: Callback,
+    panic: (err: Error, value: unknown) => void,
+    next: (err: ValidationError[], value: unknown) => void,
   ): void {
     let {
       sync,
@@ -361,15 +375,13 @@ export default abstract class Schema<
       from = [],
       originalValue = _value,
       strict = this.spec.strict,
-      abortEarly = this.spec.abortEarly,
     } = options;
 
     let value = _value;
     if (!strict) {
-      // this._validating = true;
       value = this._cast(value, { assert: false, ...options });
-      // this._validating = false;
     }
+
     // value is cast, we can check if it meets type requirements
     let args = {
       value,
@@ -387,57 +399,110 @@ export default abstract class Schema<
       if (test) initialTests.push(test);
     }
 
-    runTests(
+    this.runTests(
       {
         args,
         value,
-        path,
-        sync,
         tests: initialTests,
-        endEarly: abortEarly,
       },
-      (err) => {
-        if (err) return void cb(err, value);
-
-        runTests(
+      panic,
+      (initialErrors) => {
+        // even if we aren't ending early we can't proceed further if the types aren't correct
+        if (initialErrors.length) {
+          return next(initialErrors, value);
+        }
+
+        this.runTests(
           {
-            tests: this.tests,
             args,
-            path,
-            sync,
             value,
-            endEarly: abortEarly,
+            tests: this.tests,
           },
-          cb,
+          panic,
+          next,
         );
       },
     );
   }
 
-  // validate<U extends TType>(value: U, options?: ValidateOptions<TContext>): Promise<U>;
+  /**
+   * Executes a set of validations, either schema, produced Tests or a nested
+   * schema validate result. `args` is intended for schema validation tests, but
+   * isn't required to allow the helper to awkwardly be used to run nested array/object
+   * validations.
+   */
+  protected runTests(
+    options: TestRunOptions,
+    panic: (err: Error, value: unknown) => void,
+    next: (errors: ValidationError[], value: unknown) => void,
+  ): void {
+    let fired = false;
+    let { tests, args, value } = options;
+
+    let panicOnce = (arg: Error) => {
+      if (fired) return;
+      fired = true;
+      panic(arg, value);
+    };
+
+    let nextOnce = (arg: ValidationError[]) => {
+      if (fired) return;
+      fired = true;
+      next(arg, value);
+    };
+
+    let count = tests.length;
+    let nestedErrors = [] as ValidationError[];
+
+    if (!count) return nextOnce([]);
+
+    for (let i = 0; i < tests.length; i++) {
+      const test = tests[i];
+
+      test(args!, panicOnce, function finishTestRun(err) {
+        if (err) {
+          nestedErrors = nestedErrors.concat(err);
+        }
+        if (--count <= 0) {
+          nextOnce(nestedErrors);
+        }
+      });
+    }
+  }
+
+  asTest(value: any, options?: ValidateOptions<TContext>): RunTest {
+    // Nested validations fields are always strict:
+    //    1. parent isn't strict so the casting will also have cast inner values
+    //    2. parent is strict in which case the nested values weren't cast either
+    const testOptions = { ...options, strict: true, value };
+
+    return (_: any, panic, next) =>
+      this.resolve(testOptions)._validate(value, testOptions, panic, next);
+  }
+
   validate(
     value: any,
     options?: ValidateOptions<TContext>,
   ): Promise<this['__outputType']>;
-  validate(
-    value: any,
-    options?: ValidateOptions<TContext>,
-    maybeCb?: Callback,
-  ) {
+  validate(value: any, options?: ValidateOptions<TContext>): any {
     let schema = this.resolve({ ...options, value });
 
-    // callback case is for nested validations
-    return typeof maybeCb === 'function'
-      ? schema._validate(value, options, maybeCb)
-      : new Promise((resolve, reject) =>
-          schema._validate(value, options, (err, validated) => {
-            if (err) reject(err);
-            else resolve(validated);
-          }),
-        );
+    return new Promise((resolve, reject) =>
+      schema._validate(
+        value,
+        options,
+        (error, parsed) => {
+          if (ValidationError.isError(error)) error.value = parsed;
+          reject(error);
+        },
+        (errors, validated) => {
+          if (errors.length) reject(new ValidationError(errors!, validated));
+          else resolve(validated);
+        },
+      ),
+    );
   }
 
-  // validateSync<U extends TType>(value: U, options?: ValidateOptions<TContext>): U;
   validateSync(
     value: any,
     options?: ValidateOptions<TContext>,
@@ -449,10 +514,18 @@ export default abstract class Schema<
     let schema = this.resolve({ ...options, value });
     let result: any;
 
-    schema._validate(value, { ...options, sync: true }, (err, validated) => {
-      if (err) throw err;
-      result = validated;
-    });
+    schema._validate(
+      value,
+      { ...options, sync: true },
+      (error, parsed) => {
+        if (ValidationError.isError(error)) error.value = parsed;
+        throw error;
+      },
+      (errors, validated) => {
+        if (errors.length) throw new ValidationError(errors!, value);
+        result = validated;
+      },
+    );
 
     return result;
   }
@@ -549,9 +622,6 @@ export default abstract class Schema<
     return this.optionality(false, message);
   }
 
-  // nullable(message?: Message): any
-  // nullable(nullable: true): any
-  // nullable(nullable: false, message?: Message): any
   nullable(): any {
     return this.nullability(true);
   }
@@ -578,7 +648,7 @@ export default abstract class Schema<
   notRequired(): any {
     return this.clone().withMutation((next) => {
       next.tests = next.tests.filter(
-        (test) => test.OPTIONS.name !== 'required',
+        (test) => test.OPTIONS!.name !== 'required',
       );
       return next.nullable().optional();
     });
@@ -647,9 +717,9 @@ export default abstract class Schema<
     if (opts.name) next.exclusiveTests[opts.name] = !!opts.exclusive;
 
     next.tests = next.tests.filter((fn) => {
-      if (fn.OPTIONS.name === opts.name) {
+      if (fn.OPTIONS!.name === opts.name) {
         if (isExclusive) return false;
-        if (fn.OPTIONS.test === validate.OPTIONS.test) return false;
+        if (fn.OPTIONS!.test === validate.OPTIONS.test) return false;
       }
       return true;
     });
@@ -787,7 +857,6 @@ export default abstract class Schema<
     return next;
   }
 
-  //Schema<TType, SetFlag<TFlags, 's'>>
   strip(strip = true): any {
     let next = this.clone();
     next.spec.strip = strip;
@@ -811,7 +880,7 @@ export default abstract class Schema<
       oneOf: next._whitelist.describe(),
       notOneOf: next._blacklist.describe(),
       tests: next.tests
-        .map((fn) => ({ name: fn.OPTIONS.name, params: fn.OPTIONS.params }))
+        .map((fn) => ({ name: fn.OPTIONS!.name, params: fn.OPTIONS!.params }))
         .filter(
           (n, idx, list) => list.findIndex((c) => c.name === n.name) === idx,
         ),
diff --git a/src/types.ts b/src/types.ts
index 1020c21..3b6b9e8 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -4,8 +4,6 @@ import type { ISchema } from './util/types';
 
 export type { ISchema, AnyObject, AnySchema };
 
-export type Callback<T = any> = (err: Error | null, value?: T) => void;
-
 export type TransformFunction<T extends AnySchema> = (
   this: T,
   value: any,
@@ -40,6 +38,7 @@ export interface InternalOptions<TContext = {}>
   extends ValidateOptions<TContext> {
   __validating?: boolean;
   originalValue?: any;
+  index?: number;
   parent?: any;
   path?: string;
   sync?: boolean;
diff --git a/src/util/createValidation.ts b/src/util/createValidation.ts
index e938e3b..d67cfec 100644
--- a/src/util/createValidation.ts
+++ b/src/util/createValidation.ts
@@ -4,13 +4,18 @@ import {
   ValidateOptions,
   Message,
   InternalOptions,
-  Callback,
   ExtraParams,
 } from '../types';
 import Reference from '../Reference';
 import type { AnySchema } from '../schema';
 import { ISchema } from './types';
 
+export type PanicCallback = (err: Error) => void;
+
+export type NextCallback = (
+  err: ValidationError[] | ValidationError | null,
+) => void;
+
 export type CreateErrorOptions = {
   path?: string;
   message?: Message<any>;
@@ -53,8 +58,12 @@ export type TestConfig<TValue = unknown, TContext = {}> = {
   exclusive?: boolean;
 };
 
-export type Test = ((opts: TestOptions, cb: Callback) => void) & {
-  OPTIONS: TestConfig;
+export type Test = ((
+  opts: TestOptions,
+  panic: PanicCallback,
+  next: NextCallback,
+) => void) & {
+  OPTIONS?: TestConfig;
 };
 
 export default function createValidation(config: {
@@ -73,10 +82,11 @@ export default function createValidation(config: {
       sync,
       ...rest
     }: TestOptions<TSchema>,
-    cb: Callback,
+    panic: PanicCallback,
+    next: NextCallback,
   ) {
     const { name, test, params, message } = config;
-    let { parent, context } = options;
+    let { parent, context, abortEarly = rest.schema.spec.abortEarly } = options;
 
     function resolve<T>(item: T | Reference<T>) {
       return Ref.isRef(item) ? item.getValue(value, parent, context) : item;
@@ -106,6 +116,8 @@ export default function createValidation(config: {
       return error;
     }
 
+    const invalid = abortEarly ? panic : next;
+
     let ctx = {
       path,
       parent,
@@ -117,23 +129,31 @@ export default function createValidation(config: {
       ...rest,
     };
 
+    const handleResult = (validOrError: ReturnType<TestFunction>) => {
+      if (ValidationError.isError(validOrError)) invalid(validOrError);
+      else if (!validOrError) invalid(createError());
+      else next(null);
+    };
+
+    const handleError = (err: any) => {
+      if (ValidationError.isError(err)) invalid(err);
+      else panic(err);
+    };
+
     if (!sync) {
       try {
-        Promise.resolve(test.call(ctx, value, ctx))
-          .then((validOrError) => {
-            if (ValidationError.isError(validOrError)) cb(validOrError);
-            else if (!validOrError) cb(createError());
-            else cb(null, validOrError);
-          })
-          .catch(cb);
+        Promise.resolve(test.call(ctx, value, ctx)).then(
+          handleResult,
+          handleError,
+        );
       } catch (err: any) {
-        cb(err);
+        handleError(err);
       }
 
       return;
     }
 
-    let result;
+    let result: ReturnType<TestFunction>;
     try {
       result = test.call(ctx, value, ctx);
 
@@ -144,13 +164,11 @@ export default function createValidation(config: {
         );
       }
     } catch (err: any) {
-      cb(err);
+      handleError(err);
       return;
     }
 
-    if (ValidationError.isError(result)) cb(result);
-    else if (!result) cb(createError());
-    else cb(null, result);
+    handleResult(result);
   }
 
   validate.OPTIONS = config;
diff --git a/src/util/sortByKeyOrder.ts b/src/util/sortByKeyOrder.ts
index 94b7fbe..84ec702 100644
--- a/src/util/sortByKeyOrder.ts
+++ b/src/util/sortByKeyOrder.ts
@@ -3,12 +3,11 @@ import ValidationError from '../ValidationError';
 function findIndex(arr: readonly string[], err: ValidationError) {
   let idx = Infinity;
   arr.some((key, ii) => {
-    if (err.path?.indexOf(key) !== -1) {
+    if (err.path?.includes(key)) {
       idx = ii;
       return true;
     }
   });
-
   return idx;
 }
 
diff --git a/src/util/types.ts b/src/util/types.ts
index 31361cb..a6c9a15 100644
--- a/src/util/types.ts
+++ b/src/util/types.ts
@@ -2,11 +2,12 @@ import type { ResolveOptions } from '../Condition';
 import type { CastOptions, SchemaFieldDescription } from '../schema';
 import type {
   AnyObject,
-  Callback,
+  InternalOptions,
   Optionals,
   Preserve,
   ValidateOptions,
 } from '../types';
+import { Test } from './createValidation';
 
 export type Defined<T> = T extends undefined ? never : T;
 
@@ -19,11 +20,9 @@ export interface ISchema<T, C = AnyObject, F extends Flags = any, D = any> {
   __default: D;
 
   cast(value: any, options?: CastOptions<C>): T;
-  validate(
-    value: any,
-    options?: ValidateOptions<C>,
-    maybeCb?: Callback,
-  ): Promise<T>;
+  validate(value: any, options?: ValidateOptions<C>): Promise<T>;
+
+  asTest(value: any, options?: InternalOptions<C>): Test;
 
   describe(options?: ResolveOptions<C>): SchemaFieldDescription;
   resolve(options: ResolveOptions<C>): ISchema<T, C, F>;


Test patch:
diff --git a/src/util/runTests.ts b/src/util/runTests.ts
deleted file mode 100644
index b065a46..0000000
--- a/src/util/runTests.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import ValidationError from '../ValidationError';
-import { TestOptions } from './createValidation';
-import { Callback } from '../types';
-
-export type RunTest = (opts: TestOptions, cb: Callback) => void;
-
-export type TestRunOptions = {
-  endEarly?: boolean;
-  tests: RunTest[];
-  args?: TestOptions;
-  errors?: ValidationError[];
-  sort?: (a: ValidationError, b: ValidationError) => number;
-  path?: string;
-  value: any;
-  sync?: boolean;
-};
-
-const once = <T extends (...args: any[]) => any>(cb: T) => {
-  let fired = false;
-  return (...args: Parameters<T>) => {
-    if (fired) return;
-    fired = true;
-    cb(...args);
-  };
-};
-
-export default function runTests(options: TestRunOptions, cb: Callback): void {
-  let { endEarly, tests, args, value, errors, sort, path } = options;
-
-  let callback = once(cb);
-  let count = tests.length;
-  const nestedErrors = [] as ValidationError[];
-  errors = errors ? errors : [];
-
-  if (!count)
-    return errors.length
-      ? callback(new ValidationError(errors, value, path))
-      : callback(null, value);
-
-  for (let i = 0; i < tests.length; i++) {
-    const test = tests[i];
-
-    test(args!, function finishTestRun(err) {
-      if (err) {
-        // always return early for non validation errors
-        if (!ValidationError.isError(err)) {
-          return callback(err, value);
-        }
-        if (endEarly) {
-          err.value = value;
-          return callback(err, value);
-        }
-        nestedErrors.push(err);
-      }
-
-      if (--count <= 0) {
-        if (nestedErrors.length) {
-          if (sort) nestedErrors.sort(sort);
-
-          //show parent errors after the nested ones: name.first, name
-          if (errors!.length) nestedErrors.push(...errors!);
-          errors = nestedErrors;
-        }
-
-        if (errors!.length) {
-          callback(new ValidationError(errors!, value, path), value);
-          return;
-        }
-
-        callback(null, value);
-      }
-    });
-  }
-}
diff --git a/test-setup.js b/test-setup.js
index 6907751..20b9119 100644
--- a/test-setup.js
+++ b/test-setup.js
@@ -5,34 +5,18 @@ global.TestHelpers = require('./test/helpers');
 if (global.YUP_USE_SYNC) {
   const { Schema } = require('./src'); // eslint-disable-line global-require
 
-  const { validate } = Schema.prototype;
-
-  Schema.prototype.validate = function (value, options = {}, maybeCb) {
-    let run = false;
-
-    options.sync = true;
-
-    if (maybeCb) {
-      return validate.call(this, value, options, (...args) => {
-        if (run) {
-          return maybeCb(new Error('Did not execute synchronously'));
-        }
-
-        maybeCb(...args);
-      });
-    }
-
-    const result = new SynchronousPromise((resolve, reject) => {
-      validate.call(this, value, options, (err, value) => {
-        if (run) {
-          throw new Error('Did not execute synchronously');
-        }
-        if (err) reject(err);
-        else resolve(value);
-      });
+  const { validateSync } = Schema.prototype;
+
+  Schema.prototype.validate = function (value, options = {}) {
+    return new SynchronousPromise((resolve, reject) => {
+      let result;
+      try {
+        result = validateSync.call(this, value, options);
+      } catch (err) {
+        reject(err);
+      }
+
+      resolve(result);
     });
-
-    run = true;
-    return result;
   };
 }
diff --git a/test/object.ts b/test/object.ts
index 3a2147f..ba48d54 100644
--- a/test/object.ts
+++ b/test/object.ts
@@ -578,7 +578,7 @@ describe('Object types', () => {
       expect(inst.validate({ nest: { str: '' } })).rejects.toEqual(
         expect.objectContaining({
           value: { nest: { str: '' } },
-          path: 'nest',
+          // path: 'nest',
           errors: ['oops'],
         }),
       ),
@@ -594,6 +594,47 @@ describe('Object types', () => {
     ]);
   });
 
+  it('should flatten validation errors with abortEarly=false', async () => {
+    let inst = object({
+      str: string().required(),
+      nest: object({
+        innerStr: string().required(),
+        num: number().moreThan(5),
+        other: number().test('nested', 'invalid', () => {
+          string().email().min(3).validateSync('f', { abortEarly: false });
+          return true;
+        }),
+      }).test('name', 'oops', () => false),
+    });
+
+    const error = await inst
+      .validate(
+        { str: null, nest: { num: 2, str: undefined } },
+        { abortEarly: false },
+      )
+      .catch((e) => e);
+
+    expect(error.inner).toMatchInlineSnapshot(`
+      Array [
+        [ValidationError: str is a required field],
+        [ValidationError: nest.innerStr is a required field],
+        [ValidationError: nest.num must be greater than 5],
+        [ValidationError: oops],
+        [ValidationError: this must be a valid email],
+        [ValidationError: this must be at least 3 characters],
+      ]
+    `);
+
+    expect(error.errors).toEqual([
+      'str is a required field',
+      'nest.innerStr is a required field',
+      'nest.num must be greater than 5',
+      'oops',
+      'this must be a valid email',
+      'this must be at least 3 characters',
+    ]);
+  });
+
   it('should sort errors by insertion order', async () => {
     let inst = object({
       // use `when` to make sure it is validated second