From fee90fcfe40813141c4b853682c69f71b58627b9 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Thu, 8 Oct 2026 16:32:02 -0400 Subject: [PATCH] fix(ack): accept Draft-07 $schema URI and remove dead prefixItems index --- docs/guides/json-schema-integration.mdx | 16 +++---- packages/ack/CHANGELOG.md | 4 ++ .../src/json_schema/json_schema_compiler.dart | 15 ++++-- .../ack/test/json_schema_import_api_test.dart | 2 +- .../ack/test/json_schema_import_test.dart | 48 ++++++++++++++++++- 5 files changed, 71 insertions(+), 14 deletions(-) diff --git a/docs/guides/json-schema-integration.mdx b/docs/guides/json-schema-integration.mdx index fd0c5b0a..9e278395 100644 --- a/docs/guides/json-schema-integration.mdx +++ b/docs/guides/json-schema-integration.mdx @@ -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`. The input dialect is draft 2020-12, including -when `$schema` is absent. Other explicitly declared dialects are rejected. +`AckSchema`. 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'; diff --git a/packages/ack/CHANGELOG.md b/packages/ack/CHANGELOG.md index 1bbefa6b..b7374aaf 100644 --- a/packages/ack/CHANGELOG.md +++ b/packages/ack/CHANGELOG.md @@ -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 diff --git a/packages/ack/lib/src/json_schema/json_schema_compiler.dart b/packages/ack/lib/src/json_schema/json_schema_compiler.dart index ead38eca..f34f281a 100644 --- a/packages/ack/lib/src/json_schema/json_schema_compiler.dart +++ b/packages/ack/lib/src/json_schema/json_schema_compiler.dart @@ -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 = { @@ -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', @@ -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', ); } diff --git a/packages/ack/test/json_schema_import_api_test.dart b/packages/ack/test/json_schema_import_api_test.dart index 1cdb434d..4f67d4c5 100644 --- a/packages/ack/test/json_schema_import_api_test.dart +++ b/packages/ack/test/json_schema_import_api_test.dart @@ -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), diff --git a/packages/ack/test/json_schema_import_test.dart b/packages/ack/test/json_schema_import_test.dart index c6845f18..dae3c593 100644 --- a/packages/ack/test/json_schema_import_test.dart +++ b/packages/ack/test/json_schema_import_test.dart @@ -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()), ); } + + 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', () { @@ -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().having( + (e) => e.diagnostics.single, + 'diagnostic', + isA() + .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'},