Skip to content

Making getErrors synchronous#205

Merged
jdesrosiers merged 10 commits intohyperjump-io:mainfrom
Ahmedmhmud:making-getErrors-sync
Apr 22, 2026
Merged

Making getErrors synchronous#205
jdesrosiers merged 10 commits intohyperjump-io:mainfrom
Ahmedmhmud:making-getErrors-sync

Conversation

@Ahmedmhmud
Copy link
Copy Markdown
Contributor

This PR removes async schema value lookups from the error formatting path and makes getErrors synchronous by resolving keyword values from the compiled AST to using it so it can be used in @hyperjump/json-schema as we need here (#204).

I replaced async schema reads with two AST resolver functions: getCompiledKeywordValue() for normal keyword lookups by schemaLocation, and getSiblingKeywordValue() for sibling keywords in the same parent node (like draft-04 min/max exclusivity and contains min/max contains). Then getErrors passes this resolver to handlers, so the whole error handling path is synchronous and no longer depends on await getSchema().

getErrors now passes an AST resolver to handlers, including recursive ones, and sibling-dependent keywords use sibling lookup from the same parent AST node. I also adjusted handlers for compiled value shapes (like regex, draft-04 tuples, and const/enum JSON strings), then updated types. All tests are still passing leaves us with the same behavior.

@jdesrosiers
Copy link
Copy Markdown
Collaborator

@srivastava-diya, please do the first review

Comment thread src/error-handlers/typeConstEnum.js Outdated
Comment thread src/index.d.ts Outdated
Comment thread src/error-handlers/minimum.js Outdated
Comment thread src/error-handlers/contains.js Outdated
@Ahmedmhmud
Copy link
Copy Markdown
Contributor Author

Hi @jdesrosiers, @srivastava-diya
I did a clean up for the oxlint & JSDoc issues, is there anything else to do?

Comment thread src/index.d.ts Outdated
Comment thread src/index.d.ts Outdated
@Ahmedmhmud Ahmedmhmud force-pushed the making-getErrors-sync branch from 0ece108 to d24ba98 Compare April 13, 2026 22:47
@Ahmedmhmud
Copy link
Copy Markdown
Contributor Author

I made resolver required in ErrorHandler and getErrors, then removed resolver checks from all handlers that use it.

@srivastava-diya
Copy link
Copy Markdown
Contributor

hey @jdesrosiers all my comments have been addressed, LGTM from my side.

Comment thread src/error-handlers/pattern.js Outdated
Comment thread src/error-handlers/typeConstEnum.js
Comment thread src/json-schema-errors.js Outdated
Comment thread src/json-schema-errors.js Outdated
Comment on lines +202 to +211
/**
* @param {AST} ast
* @returns {API.ErrorResolver}
*/
const createErrorResolver = (ast) => ({
getCompiledKeywordValue: (schemaLocation) => getCompiledKeywordValue(ast, schemaLocation),
getSiblingKeywordValue: (schemaLocation, siblingKeywordUri) => {
return getSiblingKeywordValue(ast, schemaLocation, siblingKeywordUri);
}
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary. Just pass the AST instead of the resolver and call the functions directly.

Copy link
Copy Markdown
Contributor Author

@Ahmedmhmud Ahmedmhmud Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was solving the problem of recursion with anyOf, oneof and dependencies.
These handlers are calling getErrors recursively, so when I delete this method and pass the ast directly, it will be expected that the higher level has AST but the recursive calls expect another type which is ErrorResolver, but I am going to pass it and solve it by ternary condition that checks the type and selecting the value according to it.
If it suits, we will keep it in that way .

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I mean you don't need an ErrorResolver at all. It doesn't need to exist anywhere. In all cases, you can pass the AST and import the utility functions that use that AST where they are needed.

Comment thread src/json-schema-errors.js Outdated
Comment thread src/json-schema-errors.js Outdated
Comment thread src/json-schema-errors.js Outdated
Comment on lines +240 to +258
const getSiblingKeywordValue = (ast, schemaLocation, siblingKeywordUri) => {
const parentLocation = schemaLocation.replace(/\/[^/]+$/, "");
const parentNode = ast[parentLocation];

if (!Array.isArray(parentNode)) {
return undefined;
}

for (const [keywordUri, keywordLocation, keywordValue] of parentNode) {
if (keywordUri === siblingKeywordUri) {
return {
keywordLocation,
keywordValue
};
}
}

return undefined;
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's necessary this function to return the value and the location. The value will come from the AST in all cases. The only thing we need is the location of the sibling keyword.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the value is returned with the location as the handlers like contains.js needs the value but has no access to the AST, getError only has it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value that comes from the AST always includes the values you need from the sibling keyword. The min/max keywords use a tuple like [value, isExclusive] and the contains keyword uses an object like { constains, minContains, maxContains }. You don't need to get the value again. All you should ever need that you don't get from the keyword lookup is the schema location of the sibling keyword(s).

Comment thread src/json-schema-errors.js Outdated
@Ahmedmhmud
Copy link
Copy Markdown
Contributor Author

Hi @jdesrosiers
I solved all the refactor problems above and optimized with Array.find as you mentioned.
But the thing is that I left getSiblingKeywordValue as it is, because if we returned the location only, we will need the value in the handlers like contains.js, but the handles don't have access to the AST so it's better to pass the value also with the location.

Comment thread src/error-handlers/pattern.js Outdated
Comment on lines +17 to +18
const compiledPattern = resolver.getCompiledKeywordValue(schemaLocation);
const pattern = /** @type RegExp */ (compiledPattern).source;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cast should be on compiledPattern.

Comment thread src/json-schema-errors.js Outdated
Comment on lines +202 to +211
/**
* @param {AST} ast
* @returns {API.ErrorResolver}
*/
const createErrorResolver = (ast) => ({
getCompiledKeywordValue: (schemaLocation) => getCompiledKeywordValue(ast, schemaLocation),
getSiblingKeywordValue: (schemaLocation, siblingKeywordUri) => {
return getSiblingKeywordValue(ast, schemaLocation, siblingKeywordUri);
}
});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I mean you don't need an ErrorResolver at all. It doesn't need to exist anywhere. In all cases, you can pass the AST and import the utility functions that use that AST where they are needed.

Comment thread src/json-schema-errors.js Outdated
Comment on lines +240 to +258
const getSiblingKeywordValue = (ast, schemaLocation, siblingKeywordUri) => {
const parentLocation = schemaLocation.replace(/\/[^/]+$/, "");
const parentNode = ast[parentLocation];

if (!Array.isArray(parentNode)) {
return undefined;
}

for (const [keywordUri, keywordLocation, keywordValue] of parentNode) {
if (keywordUri === siblingKeywordUri) {
return {
keywordLocation,
keywordValue
};
}
}

return undefined;
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The value that comes from the AST always includes the values you need from the sibling keyword. The min/max keywords use a tuple like [value, isExclusive] and the contains keyword uses an object like { constains, minContains, maxContains }. You don't need to get the value again. All you should ever need that you don't get from the keyword lookup is the schema location of the sibling keyword(s).

@Ahmedmhmud
Copy link
Copy Markdown
Contributor Author

Hi @jdesrosiers
I removed the unnecessary ErrorResolver and passed the AST to the handlers
Now I call the resolvers in the handlers whenever I need them & this fixed also the problem of returning the value with the location of the sibling keywords since we already got the AST in the handlers.

Comment thread src/error-handlers/contains.js Outdated
Comment on lines +27 to +30
const parentLocation = schemaLocation.replace(/\/[^/]+$/, "");
const parentNode = ast[parentLocation];
const containsNode = Array.isArray(parentNode) ? parentNode.find(([, keywordLocation]) => keywordLocation === schemaLocation) : undefined;
const containsRange = /** @type {ContainsRange} */ (containsNode?.[2] ?? {});
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't you using getCompiledKeywordValue here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I used the AST and totally forgot that I'm already doing this in getCompiledKewewordValue, my bad.

Comment thread src/error-handlers/maximum.js Outdated
Comment on lines +49 to +50
const exclusiveKeywordLocation = getSiblingKeywordValue(ast, schemaLocation, "https://json-schema.org/keyword/draft-04/exclusiveMaximum");
const exclusiveLocation = exclusive && exclusiveKeywordLocation ? exclusiveKeywordLocation : "";
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only gets used if maximum < lowestMaximum && exclusive === true, so only build it in the conditional below if it's exclusive.

Comment thread src/json-schema-errors.js Outdated
};

/** @type (ast: AST, schemaLocation: string, siblingKeywordUri: string) => string | undefined */
export const getSiblingKeywordValue = (ast, schemaLocation, siblingKeywordUri) => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now returns a location, not a value. So, we need to update the function name.

@jdesrosiers jdesrosiers force-pushed the making-getErrors-sync branch from 7e68633 to 60dbebf Compare April 22, 2026 03:37
@jdesrosiers jdesrosiers merged commit 8bda546 into hyperjump-io:main Apr 22, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants