Skip to content

Simplify support for custom keywords and fix a description bug - #286

Merged
jdesrosiers merged 6 commits into
mainfrom
custom-keyword-api
Oct 6, 2026
Merged

jdesrosiers merged 6 commits into
mainfrom
custom-keyword-api

Conversation

@jdesrosiers

Copy link
Copy Markdown
Collaborator
  • Flatten simple applicator results when they're used. This fixes descriptions dropping requirements when a definition is referenced both directly and through a conditional.
  • Add defineKeyword and use it for all keywords. Remove setNormalizationHandler.
  • Add addTranslation for custom messages and locales. Untranslated messages fall back to en-US.
  • Move localization into the error handler context.
  • Reduce the helpers custom handlers need, and shorten long lists of messages in one place. anyOf/oneOf alternatives are now shortened too.
  • Update the README for custom keywords.

jdesrosiers and others added 6 commits October 5, 2026 14:16
The results of a simple applicator's subschemas were merged into the
results of the parent schema when the output was built. Descriptions
leave out the results of conditional keywords, so they were found by
matching instance location, keyword, and keyword location and removed.
A definition referenced directly and also by a conditional subschema,
such as a base definition that an admin definition extends, has the
same results through both paths, so its unconditional requirements
were dropped from descriptions whenever the condition held.

Now the output keeps results with the applicator that produced them
and they're flattened when they're used. getErrors flattens every
simple applicator. getSuccesses leaves out the results of conditional
keywords. flattenOutput is exported for handlers that inspect a
subschema's results directly, like the anyOf and oneOf filters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The startsWith example used an addErrorHandler function that doesn't
exist, passed an async function where error handlers are synchronous,
and skipped every result because it tested the keyword's result object
instead of whether it failed. It now registers the handler with
setErrorHandler, gets the keyword's value with getCompiledKeywordValue,
and has a success handler so the keyword can be described for keywords
like 'not'.

getCompiledKeywordValue is exported because error handlers otherwise
have no synchronous way to get a keyword's value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Error handlers, getErrors, and getSuccesses took a localization and a
context as separate arguments, and helpers took whichever one they
happened to need. Now the localization is part of ErrorHandlerContext,
so handlers and helpers take just the context. negate(context) gives a
context whose success messages describe what would make keywords fail,
replacing localization.negated().

describeConditional's callbacks get a context for the same reason. The
message callbacks for describeScope still take a localization because
they only build messages.

This gives the helpers consistent signatures before they're exported
for custom handlers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Messages could only come from the translations included in the package,
so custom keywords had to hard-code English messages and there was no
way to change a message or add a locale. addTranslation adds Fluent
messages to a locale, creating the locale if it doesn't exist. A message
with the same id as an existing message replaces it.

Localization has public format and formatRequirement methods for any
message, and or and and for formatting lists the same way the built-in
messages do. Messages that aren't translated for a locale fall back to
en-US instead of throwing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Custom handlers needed many of the helpers the built-in handlers use,
most of which weren't exported. This removes the need for most of them
and exports the rest.

Keyword results now include the keyword's compiled value, so handlers
don't need to look it up in the AST. getSuccesses can describe a
subschema from its schema location, which replaces
evaluateRequirements. isPassing and isFailing are replaced by
getValidity, which says whether a subschema passed, failed, or isn't
known.

Long lists of messages are now shortened in one pass over the final
messages instead of by each handler, so handlers don't need limitItems
or limitOptions. This also shortens anyOf and oneOf error alternatives,
which weren't shortened before. Options keep their order unless a group
is marked to show the smallest options first.

The exported helpers are getErrors, getSuccesses, negate, getValidity,
someTrue, allTrue, countTrue, getPlaceholder, isPlaceholder,
describeConditional, and describeScope. flattenOutput and
getCompiledKeywordValue are no longer exported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Supporting a keyword took a normalization handler and an error handler
registered separately, even for keywords that only need a message for
each occurrence that fails. defineKeyword adds a keyword with an error
message for each failing occurrence and a requirement that describes
what it requires. Applicators give an evaluate function, and
annotations are marked as annotations.

Every keyword is now defined with defineKeyword, so
setNormalizationHandler is removed and the NormalizationHandler type is
folded into KeywordDefinition. Keywords that only needed an empty
normalization handler are defined inline, and the applicators' handlers
moved to src/keywords. pattern's messages are part of its definition.
Keywords that combine occurrences into one message or are described
together still use error handlers.

Keywords are defined after the error handlers because registration
order determines message order, so pattern's messages now come last.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdesrosiers
jdesrosiers merged commit bcd3b98 into main Oct 6, 2026
2 checks passed
@jdesrosiers
jdesrosiers deleted the custom-keyword-api branch October 6, 2026 04:25
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.

1 participant