diff --git a/lib/entry-points.js b/lib/entry-points.js index e9d524358..628f26fe5 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -144518,19 +144518,41 @@ function optionalOrNull(validator) { }; } function validateSchema(schema, obj) { + const result = checkSchema(schema, obj, { failFast: true }); + return result.valid; +} +function checkSchema(schema, obj, options = {}, path29 = "") { + const result = { valid: true, unknownKeys: [] }; + const inputKeys = new Set(Object.keys(obj)); for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; if (validator.required && !hasKey) { - return false; + result.valid = false; + if (options.failFast) { + return result; + } + continue; } if (validator.required && (obj[key] === void 0 || obj[key] === null)) { - return false; + result.valid = false; + if (options.failFast) { + return result; + } + continue; } if (hasKey && !validator.validate(obj[key])) { - return false; + result.valid = false; + if (options.failFast) { + return result; + } + continue; } + inputKeys.delete(key); } - return true; + for (const remainingKey of inputKeys) { + result.unknownKeys.push(`${path29}${remainingKey}`); + } + return result; } // src/util.ts diff --git a/src/json/index.test.ts b/src/json/index.test.ts index eb259c757..5234872e5 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -67,3 +67,13 @@ test("validateSchema - optional properties are optional", async (t) => { t.true(json.validateSchema(optionalSchema, { optionalKey: "" })); t.true(json.validateSchema(optionalSchema, { optionalKey: "foo" })); }); + +test("validateSchema - checkSchema reports unknown keys", async (t) => { + const result = json.checkSchema(testSchema, { + requiredKey: "foo", + extraKey: "bar", + }); + + t.true(result.valid); + t.deepEqual(result.unknownKeys, ["extraKey"]); +}); diff --git a/src/json/index.ts b/src/json/index.ts index a55de5b58..d0a8edb51 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -121,24 +121,72 @@ export function validateSchema( schema: S, obj: UnvalidatedObject, ): obj is FromSchema { + const result = checkSchema(schema, obj, { failFast: true }); + return result.valid; +} + +export interface CheckSchemaOptions { + /** Whether to stop validation after the first error. */ + failFast?: boolean; +} + +export interface CheckSchemaResult { + /** Whether the `obj` satisfies the schema. */ + valid: boolean; + /** Unknown keys that were found during validation. */ + unknownKeys: string[]; +} + +export function checkSchema( + schema: S, + obj: UnvalidatedObject, + options: CheckSchemaOptions = {}, + path: string = "", +): CheckSchemaResult { + const result: CheckSchemaResult = { valid: true, unknownKeys: [] }; + const inputKeys = new Set(Object.keys(obj)); + for (const [key, validator] of Object.entries(schema)) { const hasKey = key in obj; // If the property is required, but absent, fail. if (validator.required && !hasKey) { - return false; + result.valid = false; + + if (options.failFast) { + return result; + } + continue; } // If the property is required, but undefined or null, fail. if (validator.required && (obj[key] === undefined || obj[key] === null)) { - return false; + result.valid = false; + + if (options.failFast) { + return result; + } + continue; } // If the property is present, validate it. if (hasKey && !validator.validate(obj[key])) { - return false; + result.valid = false; + + if (options.failFast) { + return result; + } + continue; } + + // If we reach this point, the key has been successfully validated. + inputKeys.delete(key); } - return true; + // If there are any remaining keys in `inputKeys`, add them to `unknownKeys`. + for (const remainingKey of inputKeys) { + result.unknownKeys.push(`${path}${remainingKey}`); + } + + return result; }