Combine required and dependentRequired error messages#160
Combine required and dependentRequired error messages#160Sam-61s wants to merge 3 commits intohyperjump-io:mainfrom
Conversation
jdesrosiers
left a comment
There was a problem hiding this comment.
Try again. You should end up with one error handler that handles required, dependentRequired, and the array form of dependencies. There doesn't need to be a separate error handler for each keyword. They're defined separately specifically for that reason.
|
Thanks for the clarification. I plan to unify The unified handler will consolidate missing properties into a single Does that match your expectation? |
|
Yep. That sounds right. |
|
I’ve updated the PR based on your guidance. There is now a single unified error handler in required.js that handles required, dependentRequired, and the array form of dependencies. |
jdesrosiers
left a comment
There was a problem hiding this comment.
Looks good so far. I left a few comments inline, but there's one more thing I'd like you to update in general. When contributing to project, you should try to match the code style used in the project. The way you did it isn't wrong or bad, but it looks significantly different than the rest of the error handlers. It can make it harder to review when the patterns are different. So, I'd like you to rework this a bit so it looks more like other error handlers in the repo. This includes the JSON files. Especially avoid making a lot of unnecessary whitespace changes (like in dependencies.json).
| /** @type {string[]} */ | ||
| const missingProperties = [...allMissingRequired].sort(); | ||
|
|
||
| /** @type {string[]} */ | ||
| const locations = [...allSchemaLocations].sort(); |
There was a problem hiding this comment.
No need to sort. Order doesn't matter.
src/error-handlers/required.js
Outdated
| /** @type ErrorObject[] */ | ||
| const errors = []; | ||
| const allMissingRequired = new Set(); | ||
| const allSchemaLocations = new Set(); |
There was a problem hiding this comment.
Does this need to be a Set? I'm pretty sure it's not possible to have the same schema location come up twice.
| return outputs; | ||
| return undefined; | ||
| } | ||
|
|
||
| for (const [propertyName, dependency] of dependencies) { | ||
| if (!Instance.has(propertyName, instance) || typeof dependency !== "string") { | ||
| if (!Instance.has(propertyName, instance)) { | ||
| continue; | ||
| } | ||
|
|
||
| outputs.push(evaluateSchema(dependency, instance, context)); | ||
| if (typeof dependency === "string") { | ||
| outputs.push(evaluateSchema(dependency, instance, context)); | ||
| } | ||
| } | ||
|
|
||
| return outputs; | ||
| return outputs.length > 0 ? outputs : undefined; |
There was a problem hiding this comment.
I don't think the normalization handler needs to change. What's the purpose of these changes?
| "messageParams": { | ||
| "count": 1, | ||
| "required": { "or": ["bar"] } | ||
| "required": { "and": ["bar"] } |
There was a problem hiding this comment.
Good catch! That definitely should have been an "and".
src/test-suite/tests/required.json
Outdated
| }, | ||
| { | ||
| "description": "combined required and dependentRequired", | ||
| "compatibility": ">=2019", |
There was a problem hiding this comment.
The >= isn't needed.
| "compatibility": ">=2019", | |
| "compatibility": "2019", |
There was a problem hiding this comment.
Pull request overview
This PR consolidates validation errors that previously produced multiple required-message entries when required and dependentRequired (and some dependency cases) fail at the same instance location, deduplicating the missing property list while preserving schema locations.
Changes:
- Update
requirederror handling to aggregate missing properties acrossrequired,dependentRequired, and array-form draft-04dependenciesinto a singlerequired-message. - Remove the standalone
dependentRequirederror handler and remove array-formdependenciesrequired-message generation from the draft-04dependencieserror handler. - Update/expand the JSON test-suite expectations to cover the consolidated-message behavior (including the reported issue scenario).
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/test-suite/tests/required.json | Adjusts existing required expectations and adds a new combined required+dependentRequired test case. |
| src/test-suite/tests/dependencies.json | Updates expected errors to reflect consolidated required-message behavior for dependentRequired/array-form dependencies and some dependentSchemas cases. |
| src/normalization-handlers/draft-04/dependencies.js | Minor cleanup (removes blank lines) in dependencies normalization handler. |
| src/index.js | Unregisters the removed dependentRequired error handler. |
| src/error-handlers/required.js | Implements combined missing-property aggregation across required/dependentRequired/dependencies. |
| src/error-handlers/draft-04/dependencies.js | Removes array-form dependency required-message generation; keeps dependent-schema traversal behavior. |
| src/error-handlers/dependentRequired.js | Deletes the standalone dependentRequired error handler (logic moved into required handler). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const allMissingRequired = new Set(); | ||
| const allSchemaLocations = []; | ||
|
|
||
| for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/required"]) { | ||
| if (normalizedErrors["https://json-schema.org/keyword/required"][schemaLocation]) { | ||
| continue; | ||
| const requiredErrors = normalizedErrors["https://json-schema.org/keyword/required"]; | ||
| if (requiredErrors) { | ||
| for (const schemaLocation in requiredErrors) { | ||
| if (requiredErrors[schemaLocation] === false) { | ||
| allSchemaLocations.push(schemaLocation); | ||
| const keyword = await getSchema(schemaLocation); | ||
| const required = /** @type string[] */ (Schema.value(keyword)); | ||
|
|
||
| for (const propertyName of required) { | ||
| if (!Instance.has(propertyName, instance)) { | ||
| allMissingRequired.add(propertyName); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const keyword = await getSchema(schemaLocation); | ||
| const required = /** @type string[] */ (Schema.value(keyword)); | ||
| const dependentRequiredErrors = normalizedErrors["https://json-schema.org/keyword/dependentRequired"]; | ||
| if (dependentRequiredErrors) { | ||
| for (const schemaLocation in dependentRequiredErrors) { | ||
| if (dependentRequiredErrors[schemaLocation] === false) { | ||
| allSchemaLocations.push(schemaLocation); | ||
| const keyword = await getSchema(schemaLocation); | ||
| const dependentRequired = /** @type Record<string, string[]> */ (Schema.value(keyword)); | ||
|
|
||
| const missingRequired = []; | ||
| for (const propertyName of required) { | ||
| if (!Instance.has(propertyName, instance)) { | ||
| missingRequired.push(propertyName); | ||
| for (const propertyName in dependentRequired) { | ||
| if (Instance.has(propertyName, instance)) { | ||
| for (const requiredPropertyName of dependentRequired[propertyName]) { | ||
| if (!Instance.has(requiredPropertyName, instance)) { | ||
| allMissingRequired.add(requiredPropertyName); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const dependenciesErrors = normalizedErrors["https://json-schema.org/keyword/draft-04/dependencies"]; | ||
| if (dependenciesErrors) { | ||
| for (const schemaLocation in dependenciesErrors) { | ||
| const dependencyValue = dependenciesErrors[schemaLocation]; | ||
| if (dependencyValue === false || Array.isArray(dependencyValue)) { | ||
| const keyword = await getSchema(schemaLocation); | ||
| const dependencies = /** @type Record<string, string[] | object> */ (Schema.value(keyword)); | ||
|
|
||
| let hasArrayFormDependencies = false; | ||
| for (const propertyName in dependencies) { | ||
| if (Instance.has(propertyName, instance)) { | ||
| const dependency = dependencies[propertyName]; | ||
| if (Array.isArray(dependency)) { | ||
| hasArrayFormDependencies = true; | ||
| const dependencyArray = /** @type {string[]} */ (dependency); | ||
| for (const requiredPropertyName of dependencyArray) { | ||
| if (!Instance.has(requiredPropertyName, instance)) { | ||
| allMissingRequired.add(requiredPropertyName); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (hasArrayFormDependencies) { | ||
| allSchemaLocations.push(schemaLocation); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (allMissingRequired.size > 0) { | ||
| /** @type {string[]} */ | ||
| const missingProperties = [...allMissingRequired].sort(); | ||
| /** @type {string[]} */ | ||
| const locations = [...allSchemaLocations].sort(); | ||
|
|
||
| errors.push({ | ||
| message: localization.getRequiredErrorMessage(missingRequired), | ||
| return [{ | ||
| message: localization.getRequiredErrorMessage(missingProperties), | ||
| instanceLocation: Instance.uri(instance), | ||
| schemaLocations: [schemaLocation] | ||
| }); | ||
| schemaLocations: locations | ||
| }]; | ||
| } | ||
|
|
||
| return errors; | ||
| return []; |
There was a problem hiding this comment.
The updated handler now aggregates all required failures for an instance into a single required-message (and also folds dependentRequired and array-form dependencies into that same message). This is a broader behavior change than “when required and dependentRequired fail at the same instance location”, and it will also collapse multiple distinct required keywords (e.g., via allOf or dependentSchemas) that previously produced separate error objects. If that broader consolidation is intended, it should be explicitly called out (and treated as a potentially breaking change for consumers who rely on per-keyword errors); otherwise, scope the merge logic to only combine required with dependentRequired/dependencies at the same instance location and keep separate required keyword failures as separate error objects.
When
requiredanddependentRequiredfail at the same instance location, they previously produced multiple duplicate messages. This change consolidates those failures into a singlerequired-message, combining and deduplicating missing properties while preserving schema locations.Adds test coverage for the original issue scenario and keeps existing
dependentRequiredand dependency behavior unchanged.Fixes #134