Skip to content

Positive errors - #280

Merged
jdesrosiers merged 27 commits into
mainfrom
positive-errors
Oct 5, 2026
Merged

jdesrosiers merged 27 commits into
mainfrom
positive-errors

Conversation

@jdesrosiers

Copy link
Copy Markdown
Collaborator

Resolves #120

This adds success and negation messages so we can construct messages for things that fail because something passed, like multiple oneOf alternatives passing.

This is too big to review, so I'm just going to merge it. But, please let me know if you find any issues.

This was the last blocker for an official release, so that's coming soon.

jdesrosiers and others added 27 commits October 3, 2026 16:08
A 'not' failure happens because its schema passed, so there was no
failure to report and the message could only refer to the schema.
Error handlers can now define a `success` method that describes how an
instance satisfied a keyword, and `getSuccesses` collects those
messages for a passing subschema. The 'not' error lists them as
alternatives and is worded without mentioning schemas.

Success messages are implemented for type, required (including
dependentRequired and draft-04 dependencies), and pattern. A 'not'
schema with nothing to describe gets the boolean schema message.
Nested passing applicators can't be described yet because their
subschema output isn't kept when they pass (#120).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When more than one oneOf alternative passes, the error now lists how the
instance satisfied each matching alternative so the user can see what
needs to change. Messages common to all matches are removed when that
leaves each alternative with something to say. Failing alternatives are
no longer included. If a match can't be described, the previous message
is used without alternatives.

Matches are now counted before alternatives are filtered. Previously,
passing alternatives without declared properties were filtered out,
which reported that none matched when several did.

Type messages now come first because the type error handler is
registered first, and the not message wording is adjusted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When more items match than maxContains allows, an error is now reported
on each matching item describing how it satisfied the 'contains' schema.
Reporting on the items rather than the array lets consumers such as the
language server put diagnostics on the items that need to change.

If every item matched and none can be described, the maxItems message is
used. Otherwise, if a match can't be described, the previous message is
used.

isPassing is now shared by the oneOf and contains handlers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Consumers like the language server put each top-level error at its
instanceLocation and present nested messages relative to their parent.
Every test now checks that alternatives only contain messages at or
below the location of the error they belong to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Success messages now describe what a keyword requires rather than what
is true of the instance. A message like "The value is not an object"
was misleading when a keyword passed only because the instance was the
wrong type, and describing only the matched type gave the wrong advice
when negated. dependentRequired also threw on non-objects.

Each keyword now has an error, a success, and a negated message. 'not'
uses the negated messages through `localization.negated()` and says
what is expected rather than what isn't, which is easier to understand
than negating an either/or statement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each normalized keyword result is now an object with a `valid` flag and,
for applicators, the `outputs` of their subschemas. Applicators that pass
now keep their subschema output instead of being reduced to `true`, so
handlers can describe what happened inside them.

Validator output usually only reports errors, so a keyword it doesn't
mention is assumed to pass. That isn't safe inside an applicator that
passed because the output doesn't say which subschemas passed or failed.
Results there are now unknown (`valid` is undefined) unless the output
includes them. Passing results in the output (such as `valid: true` units
or annotations) are now indexed as well so more detailed output gives
more detailed results.

This doesn't change any error messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
'not' and 'anyOf' can now describe themselves when they appear inside a
subschema that passed, such as inside a 'not' or a 'oneOf' alternative.

When it's known which subschemas passed, the messages describe only what
matters. When the validator's output doesn't say, they describe all the
possibilities instead, which is less specific but still correct.

Some descriptions need to say that at least one or all of a group of
things is true, so there are new messages for those groups. Alternatives
are now consistently a list of options where everything in an option is
true, so a 'not' error with several possibilities lists them as options.

The test suite has a new optional `errorsWithFullResults` for the errors
expected when the results of every keyword are known.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
'oneOf' can now describe itself when it appears inside a subschema that
passed. It passes when exactly one alternative matches, so describing
what would make it fail means describing how none of them could match or
how at least two of them could match.

Groups of options can now say how many of the options are true, such as
"exactly one" or "at least two", using a single message.

Describing what would make 'anyOf' fail no longer leaves out alternatives
that currently fail. Changing the value to make one alternative fail
could make another one pass, so a description that leaves them out can
lead the user to a value that's still invalid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
'contains' can now describe itself when it appears inside a subschema
that passed. It passes when the number of matching items is within its
range, so describing what would make it fail means describing how too
few or too many items could match.

When it's known which items match, the description only mentions the
items that need to change. Items are separate values, so changing one
can't change whether another one matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
'if' was treated as part of its parent schema, so its result was merged
into the parent. When validating with this package's output plugin, a
failing 'if' was reported as an error. 'then' and 'else' were merged too,
even when they didn't apply. Now 'if', 'then', and 'else' each keep the
output of their own subschema.

Validators often only report the errors in a 'then' or 'else' subschema
and not the keyword itself, so a normalization handler can now say that
the keyword's result comes from its subschema when the validator's
output doesn't include it.

The output plugin now decides whether a keyword's output is merged into
its parent based on this package's handlers rather than the validator's.

if/then/else can now describe itself when it appears inside a subschema
that passed.

Now that both ways of getting errors agree, every test in the test suite
checks the errors from full results, which default to `errors`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
maximum, exclusiveMaximum, minimum, exclusiveMinimum, multipleOf,
maxLength, minLength, maxItems, minItems, uniqueItems, maxProperties,
minProperties, const, enum, format-assertion, and draft-04 schema
dependencies can now describe what they require and what would make
them fail.

'format' isn't described because whether it's an assertion depends on how
the validator is configured, which can't be known from its output.
Describing it when it isn't an assertion would give the user false
information. 'format-assertion' is always an assertion, so it's
described.

Unknown keywords are never described because we don't know what they
require. There's now a test to make sure they stay out of descriptions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Whether 'format' is an assertion depends on how the validator is
configured and whether it supports the format. That can't be known from
the validator's output, so 'format' wasn't described at all. That could
leave a confusing generic message, such as saying no value is allowed
for {"not":{"format":"email"}} when any string that isn't an email
address would pass.

All format keywords are now described with wording that makes clear it
only applies if formats are validated. This includes 'format-assertion',
which the previous commit treated as always being an assertion, because
some validators can be configured not to validate it either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A schema dependency only applies when the value is an object with the
dependency's property, but its subschema was merged into its parent as if
it always applied. A description of what would make it fail could
suggest a change that removes the property, which would make the
dependency not apply at all. When the property was absent, there was
nothing to describe, so it fell back to a generic message that could be
wrong.

dependentSchemas now keeps the output of its subschemas and is described
like if/then. Descriptions of what would make it fail include that the
value needs to be an object with the property. Schema-form dependencies
in draft-04 work the same way.

When the validator didn't evaluate a subschema, it's now evaluated
without results so it can still be described. Success messages only
describe what keywords require, so the results aren't needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Validators usually only evaluate the 'then' or 'else' branch that
applies. The other branch had no output, so it was left out of
descriptions of what would make if/then/else fail, even though changing
the value could make it apply. It's now evaluated without results so it
can be described.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… allow anything

The anyOf and oneOf messages talked about matching "alternatives," which
requires the user to look at the schema. They now describe the listed
options instead.

When more than one oneOf alternative matched and one of them couldn't be
described, the options weren't listed at all. Alternatives that allow any
value, such as `{}` or a schema with only annotations, are now described
as allowing any value. Annotation keywords are marked as such so this can
be determined from the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
properties and prefixItems only produce results for the properties and
items that are present, so when an option matched only because they
didn't apply, it couldn't be described. Descriptions of what would make
them fail also couldn't say that a missing property or item needs to be
added.

They now record a result for themselves while still merging their
subschema results into their parent, so errors don't change. Properties
and items that aren't present are described with what they require:
either the property or item isn't there or its value passes. To describe
a value that doesn't exist yet, a placeholder is used for it.
Placeholders aren't described further, which prevents describing
recursive schemas forever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Descriptions of what made a subschema pass are shown as options the user
needs to break, such as when more than one oneOf alternative matches.
When it was known which anyOf or oneOf alternatives matched, which
keywords in a 'not' schema failed, or which way 'if' went, only those
were described. Changing the value to break what was described could
make another part pass instead, so following the description might not
fix the error.

anyOf, oneOf, not, if/then/else, and dependentSchemas now always
describe their full requirements. Known results are still used for
contains because changing one item can't change whether another item
matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
'items' with an array of schemas works like prefixItems, so items that
aren't present are now described the same way.

Items that aren't present were described using the maxItems messages,
such as "has no more than 0 items." That reads like a limit on the
number of items rather than whether there's an item at a particular
index. There are now messages that describe whether there's an item at
an index, like the ones for properties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
items, additionalItems, and draft-04 single-schema items apply a schema
to every item starting at some index. Only the items that are present
were described, so an option that matched only because there were no
such items couldn't be described, and a description of what would make
them fail couldn't say that an item could be added.

They're now described with what's required of every item they apply to,
including items that could be added. The schema is described for an item
that doesn't exist and the description is moved to the array because it
applies to any item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or every property they apply to

These keywords apply a schema to every property, or property name, in some
scope. Only the properties that are present were described, so an option
that matched only because there were no such properties couldn't be
described, and a description of what would make them fail couldn't say
that a property could be added.

They're now described with what's required of every property they apply
to, including properties that could be added. When the schema is
`false`, such as `additionalProperties: false`, it's described as not
allowing any such properties.

Looking up a location that doesn't exist can fail rather than returning
nothing, such as the name of a property that isn't present. Those
locations are now described using a placeholder like other locations
that don't exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When there weren't enough matching items, the contains message referred
to "the 'contains' schema," so the user had to look at the schema to know
what was wrong. The message now describes what an item needs to be like.

When any item would match, the problem is that the array doesn't have
enough items, so the minItems message is used. If the schema can't be
described, the message still refers to the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Several handlers described the same two shapes in their own way.
properties, prefixItems, dependentSchemas, and if/then/else describe
subschemas that only apply under some condition. items,
patternProperties, additionalProperties, and propertyNames describe a
subschema that applies to every location in some scope. Each shape is now
described by a single function so they're described consistently.

There's also now a single way to create a placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Most success handlers described each occurrence of a keyword with a
single message based on its value, each with its own copy of the same
loop. That's now done by a shared function.

`error` is now optional for error handlers like `success` is, so handlers
that only describe keywords don't need an empty `error`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some simple applicators recorded a result for themselves so they could be
described, which required a separate flag. All simple applicators now
record a result whose validity comes from their subschemas, so the flag
isn't needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The type message used "a" for every type, such as "Expected a object."
  It now uses the right article and says "a value of type" for more than
  one type.
- Messages with counts of characters, items, or properties now use the
  singular for one, and describe zero and one in a more natural way, such
  as "an empty string" or "a non-empty array."
- Descriptions of what's required of every item or property are shorter
  and don't say "at least one of the following" when there's only one
  thing that follows.
- Property names are quoted rather than following a colon in the middle
  of a sentence. How names are quoted is part of the translation.
- Removed a stray brace from the English translation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Descriptions of complex schemas can get very large, especially when
applicators are nested, and so can the work needed to build them.

Descriptions now stop at three levels of nesting and say that further
details aren't shown. Groups show at most five entries and say how many
more aren't shown. Lists of options show the smallest options first so
the simplest ones are the ones that are shown, except for the matching
oneOf alternatives, which stay in the order they appear in the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jdesrosiers
jdesrosiers merged commit be6e463 into main Oct 5, 2026
2 checks passed
@jdesrosiers
jdesrosiers deleted the positive-errors branch October 5, 2026 03:59
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.

How do we present errors when the error is that the schema(s) passed?

1 participant