Skip to content

Commit a91f869

Browse files
jdesrosiersclaude
andcommitted
Move validate to a JSE output format for @hyperjump/json-schema
Importing this package now registers a JSE output format with @hyperjump/json-schema's setOutputFormat, so validation can return error messages directly. This replaces validate and evaluateCompiledSchema. The output plugin stays in this package. JSE's output type and the locale option are added to @hyperjump/json-schema's types by augmenting OutputFormats and ValidationOptions. Whether a keyword is a simple applicator now comes from its @hyperjump/json-schema keyword definition instead of being duplicated on normalization handlers. 'then', 'else', and 'dependentSchemas' are simple applicators there, so their results are merged into their parent schema. Their normalization handlers are marked as conditional so their merged results aren't described as requirements of the parent, since their handlers describe them along with the condition. Applicators keep their subschemas' outputs so they can be described. This replaces validityFromSubschemas. This requires @hyperjump/json-schema 1.18.0, which adds setOutputFormat and no longer treats 'if' as a simple applicator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8087101 commit a91f869

28 files changed

Lines changed: 670 additions & 244 deletions

‎README.md‎

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,38 @@ console.log(errors);
5555
// ]
5656
```
5757

58+
With `@hyperjump/json-schema`, you can also get error messages directly from
59+
validation. Importing this package adds a `JSE` output format to
60+
`@hyperjump/json-schema`. It uses the full results of evaluation rather than
61+
only what the standard output formats report. Use the `locale` option to choose
62+
the language of the messages. The default is `en-US`, which is currently the
63+
only locale available.
64+
65+
```TypeScript
66+
import { registerSchema, validate } from "@hyperjump/json-schema/draft-2020-12";
67+
import { JSE } from "@hyperjump/json-schema-errors";
68+
69+
const schemaUri = "https://example.com/schema/string";
70+
registerSchema({
71+
$schema: "https://json-schema.org/draft/2020-12/schema",
72+
73+
type: "string"
74+
});
75+
76+
const output = await validate(schemaUri, 42, { outputFormat: JSE, locale: "en-US" });
77+
console.log(output);
78+
// {
79+
// valid: false,
80+
// errors: [
81+
// {
82+
// message: "Expected a string",
83+
// instanceLocation: "#",
84+
// schemaLocations: ["https://example.com/schema/string#/type"]
85+
// }
86+
// ]
87+
// }
88+
```
89+
5890
If using this package with the results from another validator, you still need to
5991
register the schema. Here's an example using `@cfworker/json-schema`.
6092

@@ -145,8 +177,10 @@ addErrorHandler(async (normalizedErrors, instance, localization) => {
145177

146178
Simple applicator keywords that just evaluate subschemas and don't make any
147179
assertions of their own don't need an error handler, only a normalization
148-
handler. For example, support for the `allOf` keyword could look like the
149-
following.
180+
handler. Whether a keyword is a simple applicator comes from the
181+
`simpleApplicator` property of its `@hyperjump/json-schema` keyword definition.
182+
The results of its subschemas are merged into the results of the parent schema.
183+
For example, support for the `allOf` keyword could look like the following.
150184

151185
```TypeScript
152186
import { setNormalizationHandler, evaluateSchema } from "@hyperjump/json-schema-errors";
@@ -156,8 +190,7 @@ const KEYWORD_URI = "https://json-schema.org/keyword/allOf";
156190
setNormalizationHandler(KEYWORD_URI, {
157191
evaluate(allOf, instance, context) {
158192
return allOf.map((schemaLocation) => evaluateSchema(schemaLocation, instance, context));
159-
},
160-
simpleApplicator: true
193+
}
161194
});
162195
```
163196

‎package-lock.json‎

Lines changed: 4 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"dependencies": {
3838
"@fluent/bundle": "^0.19.1",
3939
"@hyperjump/json-pointer": "^1.1.1",
40-
"@hyperjump/json-schema": "^1.17.2",
40+
"@hyperjump/json-schema": "^1.18.0",
4141
"@hyperjump/pact": "^1.4.0",
4242
"@hyperjump/uri": "^1.3.2",
4343
"json-stringify-deterministic": "^1.0.12"

‎src/error-handlers/dependentSchemas.js‎

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ import {
33
describeConditional,
44
evaluateRequirements,
55
getCompiledKeywordValue,
6-
getErrors,
7-
getSuccesses,
8-
mergeOutputs
6+
getSuccesses
97
} from "../json-schema-errors.js";
108

119
/**
@@ -16,23 +14,8 @@ import {
1614

1715
/** @type ErrorHandler */
1816
const dependentSchemasErrorHandler = {
19-
error: (normalizedErrors, instance, localization, ast) => {
20-
/** @type ErrorObject[] */
21-
const errors = [];
22-
23-
for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/dependentSchemas"]) {
24-
const dependentSchemas = normalizedErrors["https://json-schema.org/keyword/dependentSchemas"][schemaLocation];
25-
if (dependentSchemas.valid !== false) {
26-
continue;
27-
}
28-
29-
// Merged so errors from different dependencies can be combined
30-
const merged = mergeOutputs(dependentSchemas.outputs ?? []);
31-
errors.push(...getErrors(merged, instance, localization, ast));
32-
}
33-
34-
return errors;
35-
},
17+
// Failures in dependent schemas are merged into the parent schema's results,
18+
// so they're reported by the handlers for the keywords that failed
3619

3720
success: (normalizedOutput, instance, localization, ast) => {
3821
/** @type ErrorObject[] */

‎src/error-handlers/ifThenElse.js‎

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {
22
describeConditional,
33
evaluateRequirements,
44
getCompiledKeywordValue,
5-
getErrors,
65
getSuccesses,
76
someTrue
87
} from "../json-schema-errors.js";
@@ -15,25 +14,8 @@ import {
1514

1615
/** @type ErrorHandler */
1716
const ifThenElseErrorHandler = {
18-
error: (normalizedErrors, instance, localization, ast) => {
19-
/** @type ErrorObject[] */
20-
const errors = [];
21-
22-
for (const keywordUri of ["https://json-schema.org/keyword/then", "https://json-schema.org/keyword/else"]) {
23-
for (const schemaLocation in normalizedErrors[keywordUri]) {
24-
const keywordOutput = normalizedErrors[keywordUri][schemaLocation];
25-
if (keywordOutput.valid !== false) {
26-
continue;
27-
}
28-
29-
for (const subschemaOutput of keywordOutput.outputs ?? []) {
30-
errors.push(...getErrors(subschemaOutput, instance, localization, ast));
31-
}
32-
}
33-
}
34-
35-
return errors;
36-
},
17+
// Failures in 'then' and 'else' are merged into the parent schema's results,
18+
// so they're reported by the handlers for the keywords that failed
3719

3820
success: (normalizedOutput, instance, localization, ast) => {
3921
/** @type ErrorObject[] */

‎src/hyperjump-json-schema.test.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import "@hyperjump/json-schema/draft-06";
1212
import "@hyperjump/json-schema/draft-04";
1313
import "@hyperjump/json-schema/formats";
1414
import { BASIC } from "@hyperjump/json-schema/experimental";
15-
import { jsonSchemaErrors, validate as validateWithErrors } from "../src/index.js";
15+
import { JSE, jsonSchemaErrors } from "../src/index.js";
1616
import { FluentBundle, FluentResource } from "@fluent/bundle";
1717
import { translations } from "./translations/index.js";
1818

@@ -88,7 +88,7 @@ const runTests = (dialectUri, dialect) => {
8888
expect(errors).to.eql(buildErrors(testCase.errors, schemaUri));
8989
expectNestedLocations(errors);
9090

91-
const result = await validateWithErrors(schemaUri, instance);
91+
const result = await validate(schemaUri, instance, JSE);
9292
const fullResultsErrors = result.valid ? [] : result.errors;
9393
expect(fullResultsErrors).to.eql(buildErrors(testCase.errorsWithFullResults ?? testCase.errors, schemaUri));
9494
expectNestedLocations(fullResultsErrors);

‎src/index.d.ts‎

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AST, CompiledSchema, EvaluationPlugin } from "@hyperjump/json-schema/experimental";
1+
import { AST, EvaluationPlugin } from "@hyperjump/json-schema/experimental";
22
import { JsonNode } from "@hyperjump/json-schema/instance/experimental";
33
import { Localization } from "./localization.js";
44

@@ -89,25 +89,15 @@ export type NormalizationHandler<KeywordValue = unknown, Context extends Evaluat
8989
evaluate(value: KeywordValue, instance: JsonNode, context: Context): NormalizedOutput[] | void;
9090

9191
/**
92-
* Simple applicators just apply subschemas and don't have any validation behavior
93-
* of their own. For example, `allOf` and `properties` are simple applicators. They
94-
* never fail. Only their subschema can fail. `anyOf` and `oneOf` are not simple
95-
* applicators because they can fail independently of the validation result of
96-
* their subschemas.
97-
*
98-
* The results of a simple applicator's subschemas are merged into the results
99-
* of its parent schema. Its own result is recorded too so it can be described,
100-
* and like `validityFromSubschemas`, it fails if any of its subschemas fail.
101-
*/
102-
simpleApplicator?: true;
103-
104-
/**
105-
* Some applicators, like `then` and `else`, only fail when their subschema
106-
* fails. Validators often only report the subschema's errors and not the
107-
* keyword itself, so this tells us to use the subschema's results when the
108-
* validator's output doesn't include the keyword.
92+
* Conditional applicators, like `then` and `dependentSchemas`, are simple
93+
* applicators whose subschemas only apply when a condition holds. Whether a
94+
* keyword is a simple applicator comes from its `@hyperjump/json-schema`
95+
* keyword definition. The results of a simple applicator's subschemas are
96+
* merged into the results of its parent schema, but the merged results of a
97+
* conditional applicator are left out when describing the parent schema
98+
* because its error handler describes them along with the condition.
10999
*/
110-
validityFromSubschemas?: true;
100+
conditional?: true;
111101

112102
/**
113103
* Annotations, such as `title` and `description`, never affect validation. A
@@ -224,39 +214,33 @@ export type ContainsRange = {
224214
};
225215

226216
/**
227-
* Validate an instance against a schema and get error messages in one step instead
228-
* of getting output from validation and passing it to jsonSchemaErrors. The
229-
* function is curried so you can compile the schema one time and evaluate multiple
230-
* instances against the same compiled schema.
217+
* An output format for `@hyperjump/json-schema` that returns human readable error
218+
* messages. Importing this package registers it.
231219
*
232-
* Ideally, this function should be in @hyperjump/json-schema instead and this will
233-
* be removed in the future.
234-
*
235-
* @deprecated
220+
* @example
221+
* const output = await validate(schemaUri, instance, { outputFormat: JSE, locale: "en-US" });
236222
*/
237-
export const validate: (
238-
(schemaUri: string) => Promise<EvaluateInstance>
239-
) & (
240-
(schemaUri: string, instance: Json, options?: ValidationOptions) => Promise<ValidationResult>
241-
);
242-
243-
export const evaluateCompiledSchema: (compiledSchema: CompiledSchema, instance: Json, options?: ValidationOptions) => ValidationResult;
244-
245-
export type EvaluateInstance = (instance: Json, options?: ValidationOptions) => ValidationResult;
246-
247-
export type ValidationOptions = {
248-
/**
249-
* A locale identifier in the form of "{language}-{region}".
250-
*
251-
* @example "en-US"
252-
*/
253-
locale?: string;
254-
plugins?: EvaluationPlugin[];
255-
};
223+
export const JSE: "JSE";
256224

257-
export type ValidationResult = {
225+
export type JSEOutput = {
258226
valid: true;
259227
} | {
260228
valid: false;
261229
errors: JsonSchemaErrors;
262230
};
231+
232+
declare module "@hyperjump/json-schema" {
233+
interface OutputFormats {
234+
JSE: JSEOutput;
235+
}
236+
237+
interface ValidationOptions<F extends OutputFormat = OutputFormat> {
238+
/**
239+
* A locale identifier in the form of "{language}-{region}" used for the
240+
* messages of the JSE output format.
241+
*
242+
* @example "en-US"
243+
*/
244+
locale?: string;
245+
}
246+
}

‎src/index.js‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,6 @@ export {
191191
jsonSchemaErrors,
192192
removeErrorHandler,
193193
setErrorHandler,
194-
setNormalizationHandler,
195-
validate,
196-
evaluateCompiledSchema
194+
setNormalizationHandler
197195
} from "./json-schema-errors.js";
196+
export { JSE } from "./output-format.js";

0 commit comments

Comments
 (0)