Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions docs/guides/json-schema-integration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ Your Ack [schema](../core-concepts/schemas.mdx) already describes your data's sh

Use strict `Ack.fromJsonSchema()` to turn a decoded JSON Schema document
(a map or boolean, not a JSON string) into an
`AckSchema<Object, Object>`. The input dialect is draft 2020-12, including
when `$schema` is absent. Other explicitly declared dialects are rejected.
`AckSchema<Object, Object>`. The input dialect is draft 2020-12 or Draft-7
(`http(s)://json-schema.org/draft-07/schema(#)`), including when `$schema` is
absent. Other explicitly declared dialects are rejected.

Only the declared `$schema` string is checked — a document is never matched
against a dialect by shape. Replacing an older draft's `$schema` string does
not convert its semantics. Tuple-form `items` (a Draft-7 array) is rejected;
Draft-7 ignores `$ref` siblings that this importer applies. Draft-4 boolean
`exclusiveMinimum` and `exclusiveMaximum` values are rejected, and Draft-4
`integer` rejects `1.0` where this importer accepts it. Convert older schemas
to draft 2020-12 and check their validation results before importing them.
against a dialect by shape. Draft-7 documents are evaluated under the same
supported subset: tuple-form `items` (a Draft-7 array) is rejected, and `$ref`
siblings are applied as in draft 2020-12. Draft-4 boolean `exclusiveMinimum`
and `exclusiveMaximum` values are rejected, and Draft-4 `integer` rejects `1.0`
where this importer accepts it.

```dart
import 'package:ack/ack.dart';
Expand Down
4 changes: 4 additions & 0 deletions packages/ack/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
`double`), and accepts `x-*` vendor extension annotations (such as
`x-transformed`). Unknown format labels and non-`x-*` extension keywords
continue to fail import with `unsupported_keyword`.
* `Ack.fromJsonSchema()` now accepts Draft-07 `$schema` URIs
(`http(s)://json-schema.org/draft-07/schema` with or without a trailing `#`),
so schemas exported by `.toJsonSchema()` can be re-imported without stripping
`$schema`.

### Changed

Expand Down
15 changes: 11 additions & 4 deletions packages/ack/lib/src/json_schema/json_schema_compiler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ final class _JsonSchemaCompiler {
'then',
'else',
};
static const listKeywords = {'anyOf', 'allOf', 'oneOf', 'prefixItems'};
static const listKeywords = {'anyOf', 'allOf', 'oneOf'};

// The subsets of the indexing sets above that validation implements.
static const supportedChildKeywords = {
Expand Down Expand Up @@ -166,6 +166,14 @@ final class _JsonSchemaCompiler {
'exclusiveMinimum',
'exclusiveMaximum',
};
static const supportedSchemaDialects = {
'https://json-schema.org/draft/2020-12/schema',
'https://json-schema.org/draft/2020-12/schema#',
'http://json-schema.org/draft-07/schema',
'http://json-schema.org/draft-07/schema#',
'https://json-schema.org/draft-07/schema',
'https://json-schema.org/draft-07/schema#',
};
static const supportedFormats = {
'date-time',
'date',
Expand Down Expand Up @@ -247,12 +255,11 @@ final class _JsonSchemaCompiler {
if (source.containsKey(r'$schema')) {
final dialect = source[r'$schema'];
// An empty fragment identifies the same meta-schema resource.
if (dialect != 'https://json-schema.org/draft/2020-12/schema' &&
dialect != 'https://json-schema.org/draft/2020-12/schema#') {
if (!supportedSchemaDialects.contains(dialect)) {
_fail(
node,
r'$schema',
'Only draft 2020-12 input is supported.',
'Only draft 2020-12 and draft-07 inputs are supported.',
code: 'unsupported_dialect',
);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/ack/test/json_schema_import_api_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ void main() {
{'minLength': -1},
{r'$ref': 'missing.json'},
{r'$ref': '#'},
{r'$schema': 'http://json-schema.org/draft-07/schema#'},
{r'$schema': 'http://json-schema.org/draft-04/schema#'},
]) {
expect(
() => Ack.fromJsonSchema(document),
Expand Down
48 changes: 47 additions & 1 deletion packages/ack/test/json_schema_import_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,34 @@ void main() {
{r'$ref': '#/%FF'},
{r'$schema': null},
{r'$anchor': null},
{r'$schema': 'http://json-schema.org/draft-07/schema#'},
{r'$schema': 'http://json-schema.org/draft-04/schema#'},
]) {
expect(
() => Ack.fromJsonSchema(document),
throwsA(isA<JsonSchemaImportException>()),
);
}

for (final draft07Uri in [
'http://json-schema.org/draft-07/schema#',
'http://json-schema.org/draft-07/schema',
'https://json-schema.org/draft-07/schema#',
'https://json-schema.org/draft-07/schema',
]) {
final draft07Schema = Ack.fromJsonSchema({
r'$schema': draft07Uri,
'definitions': {
'item': {'type': 'string', 'minLength': 2},
},
'type': 'object',
'properties': {
'name': {r'$ref': '#/definitions/item'},
},
'required': ['name'],
});
expect(draft07Schema.safeParse({'name': 'Ada'}).isOk, isTrue);
expect(draft07Schema.safeParse({'name': 'A'}).isFail, isTrue);
}
});

test('attributes duplicate anchors to the second anchor keyword', () {
Expand Down Expand Up @@ -1208,6 +1229,31 @@ void main() {
},
);

test('keeps prefixItems unsupported without indexing as a schema list', () {
for (final document in [
{
'type': 'array',
'prefixItems': [
{'type': 'string'},
],
},
{'type': 'array', 'prefixItems': 'not-a-list'},
]) {
expect(
() => Ack.fromJsonSchema(document),
throwsA(
isA<JsonSchemaImportException>().having(
(e) => e.diagnostics.single,
'diagnostic',
isA<JsonSchemaImportDiagnostic>()
.having((d) => d.code, 'code', 'unsupported_keyword')
.having((d) => d.keyword, 'keyword', 'prefixItems'),
),
),
);
}
});

test('rejects malformed supported keywords', () {
for (final document in [
{'type': 'made-up'},
Expand Down
Loading