diff --git a/docs/api-reference/index.mdx b/docs/api-reference/index.mdx index 767cb3b3..7287de10 100644 --- a/docs/api-reference/index.mdx +++ b/docs/api-reference/index.mdx @@ -36,10 +36,9 @@ Entry point for creating schemas. See [Schema Types](../core-concepts/schemas.md - `Ack.any()`: Creates an `AnySchema` that accepts any non-null JSON-safe value. Chain `.nullable()` to allow `null`. - `Ack.fromJsonSchema(Object document, {Uri? baseUri, Map - documents = const {}})`: Strictly imports a decoded draft 2020-12 - JSON Schema. Referenced documents must be supplied explicitly; Ack never - fetches files or URLs. The supported `date-time` format is asserted; see - [format import](../guides/json-schema-integration.mdx#importing-format). + documents = const {}, bool assertFormats = false})`: Imports a decoded draft 2020-12 JSON + Schema. Referenced documents must be supplied explicitly; Ack never fetches + files or URLs. Formats are annotations unless assertions are enabled. - `Ack.map(AckSchema valueSchema)`: Creates a `MapSchema` for a JSON object with arbitrary string keys whose values all match `valueSchema`. Use `Ack.map(Ack.any().nullable())` for a `JsonMap`. @@ -121,10 +120,16 @@ For non-`num` boundary types (e.g. numeric strings), use ### Utility Methods -- `Map toJsonSchema()`: Returns a Draft-7 JSON Schema map via - the canonical `AckSchemaModel` boundary. +- `Map toJsonSchema()`: Returns a JSON Schema representation + via the canonical `AckSchemaModel` boundary, preserving imported Draft 2020-12 + resources by default and emitting Draft-7 for native schemas. +- `Map toJsonSchemaDraft7()`: Exports imported schemas using + the legacy Draft-7 `definitions` / `$ref` lowering when possible. - `AckSchemaModel toSchemaModel()`: Returns the canonical, target-independent - boundary model for schema adapters, including export warnings. + boundary model for schema adapters, preserving imported Draft 2020-12 + resources by default. +- `AckSchemaModel toSchemaModelDraft7()`: Returns the matching model for the + legacy Draft-7 lowering. - `Map toMap()`: Serializes the schema for debugging. See also [Schema Types](../core-concepts/schemas.mdx) for detailed usage examples. @@ -499,7 +504,7 @@ Canonical export model for Ack schemas. - Created by `schema.toSchemaModel()`. - Represents the boundary/export shape, JSON-compatible defaults, discriminator metadata, target-independent constraints, and export warnings. -- `schema.toSchemaModel().toJsonSchema()` returns the same Draft-7 map as `schema.toJsonSchema()`. +- `schema.toSchemaModel().toJsonSchema()` returns the same JSON Schema map as `schema.toJsonSchema()`. Use `toSchemaModelDraft7()` for legacy Draft-7 lowering of imported schemas. - Adapters that need a JSON map should call `schema.toJsonSchema()`; adapters for non-JSON targets should convert from `AckSchemaModel` rather than traversing `AckSchema` subclasses directly. ### `AnySchema` diff --git a/packages/ack/CHANGELOG.md b/packages/ack/CHANGELOG.md index b7374aaf..54791c1e 100644 --- a/packages/ack/CHANGELOG.md +++ b/packages/ack/CHANGELOG.md @@ -2,6 +2,14 @@ ### Added +* `Ack.fromJsonSchema()` now validates and round-trips the complete required + Draft 2020-12 test suite, including dynamic references, unevaluated keywords, + and offline standard meta-schemas. `assertFormats: true` enables optional + format assertions. +* `toJsonSchema()` and `toSchemaModel()` now preserve imported Draft 2020-12 + resources by default. `toJsonSchemaDraft7()` and `toSchemaModelDraft7()` + retain the legacy 1.x Draft-7 lowering for imported schemas when compatible; + imports that require 2020-12-only semantics reject Draft-7 lowering explicitly. * `Ack.fromJsonSchema()` now supports `multipleOf` for `number` and `integer` schemas. Non-positive or non-numeric `multipleOf` values fail import with an `invalid_schema` diagnostic. diff --git a/packages/ack/lib/src/ack.dart b/packages/ack/lib/src/ack.dart index af4f7892..0b6aa7b4 100644 --- a/packages/ack/lib/src/ack.dart +++ b/packages/ack/lib/src/ack.dart @@ -11,16 +11,20 @@ final class Ack { /// /// [document] must be a map or boolean, not a JSON string. Referenced /// [documents] are supplied by retrieval URI; no files or URLs are fetched. - /// The supported `date-time` format is asserted. Other formats remain - /// unsupported. Unsupported semantics throw [JsonSchemaImportException]. + /// [assertFormats] opts into format assertions under the standard dialect; + /// a supplied dialect requiring the format-assertion vocabulary also enables + /// them. Unknown asserted formats are rejected when compiling. + /// Unsupported semantics throw [JsonSchemaImportException]. static AckSchema fromJsonSchema( Object document, { Uri? baseUri, Map documents = const {}, + bool assertFormats = false, }) => ImportedJsonSchema.fromDocument( document, baseUri: baseUri, documents: documents, + assertFormats: assertFormats, ); /// Creates a string schema. Boundary and runtime are both `String`. 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 f34f281a..8a19d5af 100644 --- a/packages/ack/lib/src/json_schema/json_schema_compiler.dart +++ b/packages/ack/lib/src/json_schema/json_schema_compiler.dart @@ -13,42 +13,38 @@ String _keywordPointer(_ImportedNode node, String keyword) { } final class _ImportedNode { - _ImportedNode(this.source, this.documentUri, this.pointer, this.baseUri); + _ImportedNode( + this.source, + this.documentUri, + this.pointer, + this.baseUri, + this.dialectUri, + ); final Object source; final Uri documentUri; final String pointer; final Uri baseUri; - - /// Keyword values and sub-schemas in document order. [render] and - /// [dependencies] read these; validation reads the typed fields below. + final Uri dialectUri; + bool validationVocabulary = true; + bool formatAssertion = false; final keywords = {}; final children = {}; final maps = >{}; final lists = >{}; - - // Validation reads these typed fields, which the compiler assigns in the - // same branch that checks each keyword. - bool get isFalse => source == false; _ImportedNode? reference; - RegExp? pattern; + _ImportedNode? dynamicReference; + String? dynamicReferenceName; + String? dynamicAnchorName; + late _ImportedNode resourceRoot; + final dynamicAnchors = {}; + + // Typed fields for validation, avoiding per-value map lookups. + bool get isFalse => identical(source, false); List? types; bool hasConst = false; Object? constValue; List? enumValues; - List<_ImportedNode>? allOf; - List<_ImportedNode>? anyOf; - List<_ImportedNode>? oneOf; - _ImportedNode? not; - _ImportedNode? ifNode; - _ImportedNode? thenNode; - _ImportedNode? elseNode; - _ImportedNode? items; - _ImportedNode? contains; - _ImportedNode? additionalProperties; - _ImportedNode? propertyNames; - Map? properties; - List? required; num? minimum; num? maximum; num? exclusiveMinimum; @@ -60,11 +56,34 @@ final class _ImportedNode { num? maxItems; num? minProperties; num? maxProperties; + num? minContains; + num? maxContains; bool uniqueItems = false; + RegExp? pattern; String? format; + List? required; + Map>? dependentRequired; + Map? properties; + List<(String, RegExp, _ImportedNode)>? patternProperties; + Map? dependentSchemas; + _ImportedNode? items; + _ImportedNode? additionalProperties; + _ImportedNode? not; + _ImportedNode? contains; + _ImportedNode? propertyNames; + _ImportedNode? unevaluatedProperties; + _ImportedNode? unevaluatedItems; + _ImportedNode? ifNode; + _ImportedNode? thenNode; + _ImportedNode? elseNode; + List<_ImportedNode>? anyOf; + List<_ImportedNode>? allOf; + List<_ImportedNode>? oneOf; + List<_ImportedNode>? prefixItems; List<_ImportedNode> get dependencies => [ ?reference, + ?dynamicReference, ...children.values, for (final map in maps.values) ...map.values, for (final list in lists.values) ...list, @@ -73,6 +92,7 @@ final class _ImportedNode { /// Sub-schemas applied to the same instance, for the cycle check. List<_ImportedNode> get inPlaceDependencies => [ ?reference, + ?dynamicReference, ?not, ?ifNode, ?thenNode, @@ -80,10 +100,21 @@ final class _ImportedNode { ...?anyOf, ...?allOf, ...?oneOf, + ...?dependentSchemas?.values, ]; - /// Renders this node as Draft-7, with [refOf] supplying each `$ref` target. - Map render(String Function(_ImportedNode) refOf) { + /// The 1.x Draft-7 representation of an imported node. Newer 2020-12 + /// assertions that cannot be lowered fail when `toJsonSchemaDraft7()` is used. + Map renderLegacy(String Function(_ImportedNode) refOf) { + if (dynamicReference != null || + dynamicAnchorName != null || + (source is Map && + (source as Map).containsKey('format'))) { + throw UnsupportedError( + 'This imported schema needs 2020-12 semantics and cannot be lowered ' + 'with toJsonSchemaDraft7().', + ); + } if (isFalse) return {'not': {}}; // Empty enums are valid in 2020-12 but not Draft-7's meta-schema. if (keywords['enum'] case []) return {'not': {}}; @@ -104,10 +135,21 @@ final class _ImportedNode { } final class _JsonSchemaCompiler { + _JsonSchemaCompiler({this.assertFormats = false}); + + final bool assertFormats; + static final standardDialect = Uri.parse( + 'https://json-schema.org/draft/2020-12/schema', + ); + static const validationVocabularyUri = + 'https://json-schema.org/draft/2020-12/vocab/validation'; + static const formatAssertionVocabularyUri = + 'https://json-schema.org/draft/2020-12/vocab/format-assertion'; final diagnostics = []; final locations = <(Uri, String), _ImportedNode>{}; final resources = {}; final anchors = {}; + final dynamicAnchors = {}; static const mapKeywords = { r'$defs', @@ -117,21 +159,6 @@ final class _JsonSchemaCompiler { 'dependentSchemas', }; static const childKeywords = { - 'items', - 'additionalProperties', - 'not', - 'contains', - 'propertyNames', - 'unevaluatedProperties', - 'unevaluatedItems', - 'if', - 'then', - 'else', - }; - static const listKeywords = {'anyOf', 'allOf', 'oneOf'}; - - // The subsets of the indexing sets above that validation implements. - static const supportedChildKeywords = { 'items', 'additionalProperties', 'not', @@ -140,8 +167,10 @@ final class _JsonSchemaCompiler { 'if', 'then', 'else', + 'unevaluatedProperties', + 'unevaluatedItems', }; - static const supportedListKeywords = {'anyOf', 'allOf', 'oneOf'}; + static const listKeywords = {'anyOf', 'allOf', 'oneOf', 'prefixItems'}; static const annotations = { 'title', 'description', @@ -151,6 +180,20 @@ final class _JsonSchemaCompiler { 'readOnly', 'writeOnly', 'deprecated', + 'format', + 'contentEncoding', + 'contentMediaType', + 'contentSchema', + }; + static const knownVocabularies = { + 'https://json-schema.org/draft/2020-12/vocab/core', + 'https://json-schema.org/draft/2020-12/vocab/applicator', + 'https://json-schema.org/draft/2020-12/vocab/unevaluated', + 'https://json-schema.org/draft/2020-12/vocab/validation', + 'https://json-schema.org/draft/2020-12/vocab/meta-data', + 'https://json-schema.org/draft/2020-12/vocab/format-annotation', + formatAssertionVocabularyUri, + 'https://json-schema.org/draft/2020-12/vocab/content', }; static const counts = { 'minLength', @@ -166,29 +209,6 @@ 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', - 'email', - 'uuid', - 'uri', - 'ipv4', - 'ipv6', - 'int32', - 'int64', - 'uint32', - 'uint64', - 'float', - 'double', - }; _ImportedNode addDocument(Object document, Uri uri) { if (uri.hasFragment && uri.fragment.isNotEmpty) { @@ -220,6 +240,7 @@ final class _JsonSchemaCompiler { uri.removeFragment(), '#', uri.removeFragment(), + dialect: standardDialect, isRoot: true, ); } @@ -229,10 +250,12 @@ final class _JsonSchemaCompiler { Uri document, String pointer, Uri base, { + Uri? dialect, bool isRoot = false, }) { if (locations[(document, pointer)] case final existing?) return existing; - final provisional = _ImportedNode(source, document, pointer, base); + dialect ??= standardDialect; + final provisional = _ImportedNode(source, document, pointer, base, dialect); if (source is! bool && source is! Map) { _fail(provisional, '', 'Expected a schema object or boolean.'); } @@ -245,25 +268,24 @@ final class _JsonSchemaCompiler { } base = base.removeFragment(); } - final node = _ImportedNode(source, document, pointer, base); + if (source is Map && source.containsKey(r'$schema')) { + final declared = source[r'$schema']; + if (declared is! String) { + _fail(provisional, r'$schema', 'Expected a URI string.'); + } + final resolved = _resolve(provisional, r'$schema', base, declared); + if (resolved.fragment.isNotEmpty) { + _fail(provisional, r'$schema', 'Dialect URIs must not have fragments.'); + } + dialect = resolved.removeFragment(); + } + final node = _ImportedNode(source, document, pointer, base, dialect); locations[(document, pointer)] = node; if (isRoot) _register(resources, document, node, r'$id'); if (isRoot || (source is Map && source.containsKey(r'$id'))) { _register(resources, base, node, r'$id'); } if (source is! Map) return node; - if (source.containsKey(r'$schema')) { - final dialect = source[r'$schema']; - // An empty fragment identifies the same meta-schema resource. - if (!supportedSchemaDialects.contains(dialect)) { - _fail( - node, - r'$schema', - 'Only draft 2020-12 and draft-07 inputs are supported.', - code: 'unsupported_dialect', - ); - } - } if (source.containsKey(r'$anchor')) { final anchor = source[r'$anchor']; if (anchor is! String || @@ -272,6 +294,17 @@ final class _JsonSchemaCompiler { } _register(anchors, base.replace(fragment: anchor), node, r'$anchor'); } + if (source.containsKey(r'$dynamicAnchor')) { + final anchor = source[r'$dynamicAnchor']; + if (anchor is! String || + !RegExp(r'^[A-Za-z_][-A-Za-z0-9._]*$').hasMatch(anchor)) { + _fail(node, r'$dynamicAnchor', 'Invalid dynamic anchor name.'); + } + node.dynamicAnchorName = anchor; + final uri = base.replace(fragment: anchor); + _register(dynamicAnchors, uri, node, r'$dynamicAnchor'); + _register(anchors, uri, node, r'$dynamicAnchor'); + } for (final entry in source.entries) { final key = entry.key; final value = entry.value; @@ -286,10 +319,17 @@ final class _JsonSchemaCompiler { document, '$path/${_importPointerToken(child.key)}', base, + dialect: dialect, ); } } else if (childKeywords.contains(key)) { - _index(value ?? _invalidSchema(node, key), document, path, base); + _index( + value ?? _invalidSchema(node, key), + document, + path, + base, + dialect: dialect, + ); } else if (listKeywords.contains(key)) { if (value is! List || value.isEmpty) { _fail(node, key, 'Expected a non-empty list of schemas.'); @@ -300,6 +340,7 @@ final class _JsonSchemaCompiler { document, '$path/$i', base, + dialect: dialect, ); } } @@ -333,20 +374,81 @@ final class _JsonSchemaCompiler { } void compile(_ImportedNode root) { + for (final node in locations.values) { + _configureDialect(node); + } final nodes = _compileReachable(root); + for (final node in locations.values) { + node.resourceRoot = resources[node.baseUri]!; + } + for (final entry in dynamicAnchors.entries) { + resources[entry.key.removeFragment()]!.dynamicAnchors[Uri.decodeComponent( + entry.key.fragment, + )] = + entry.value; + } _checkProductiveCycles(nodes); } + void _configureDialect(_ImportedNode node) { + final dialect = resources[node.dialectUri]; + if (dialect == null) { + _fail( + node, + r'$schema', + 'Unsupported or unsupplied dialect: ${node.dialectUri}.', + code: 'unsupported_dialect', + ); + } + final vocabulary = switch (dialect.source) { + final Map source => source[r'$vocabulary'], + _ => null, + }; + if (vocabulary is! Map) { + _fail( + node, + r'$schema', + 'Dialect has no declared vocabularies: ${node.dialectUri}.', + code: 'unsupported_dialect', + ); + } + for (final entry in vocabulary.entries) { + if (entry.value == true && !knownVocabularies.contains(entry.key)) { + _fail( + node, + r'$schema', + 'Unknown required vocabulary: ${entry.key}.', + code: 'unsupported_vocabulary', + ); + } + } + node.validationVocabulary = vocabulary.containsKey(validationVocabularyUri); + node.formatAssertion = + assertFormats || vocabulary.containsKey(formatAssertionVocabularyUri); + } + List<_ImportedNode> _compileReachable(_ImportedNode root) { final nodes = <_ImportedNode>[]; final pending = Queue<_ImportedNode>()..add(root); final compiled = <_ImportedNode>{}; - while (pending.isNotEmpty) { - final node = pending.removeFirst(); - if (!compiled.add(node)) continue; - _compileNode(node); - nodes.add(node); - pending.addAll(node.dependencies); + while (true) { + while (pending.isNotEmpty) { + final node = pending.removeFirst(); + if (!compiled.add(node)) continue; + _compileNode(node); + nodes.add(node); + pending.addAll(node.dependencies); + } + if (!nodes.any((node) => node.dynamicReference != null)) break; + final activeResources = nodes.map((node) => node.baseUri).toSet(); + final anchorsToCompile = dynamicAnchors.entries + .where( + (entry) => activeResources.contains(entry.key.removeFragment()), + ) + .map((entry) => entry.value) + .where((node) => !compiled.contains(node)); + if (anchorsToCompile.isEmpty) break; + pending.addAll(anchorsToCompile); } return nodes; } @@ -405,7 +507,15 @@ final class _JsonSchemaCompiler { // A reference can designate a schema inside an extension container, such // as A2UI's /components/Text. Only explicitly targeted locations become // schemas; objects in defaults and enum values are never scanned as schemas. - return _index(value!, resource.documentUri, pointer, base); + final indexed = _index( + value!, + resource.documentUri, + pointer, + base, + dialect: resource.dialectUri, + ); + _configureDialect(indexed); + return indexed; } void _compileNode(_ImportedNode node) { @@ -418,6 +528,7 @@ final class _JsonSchemaCompiler { r'$id', r'$schema', r'$anchor', + r'$dynamicAnchor', r'$defs', 'definitions', }.contains(key)) { @@ -425,14 +536,48 @@ final class _JsonSchemaCompiler { } if (annotations.contains(key)) { final valid = switch (key) { - 'title' || 'description' || r'$comment' => value is String, + 'title' || + 'description' || + r'$comment' || + 'format' || + 'contentEncoding' || + 'contentMediaType' => value is String, 'readOnly' || 'writeOnly' || 'deprecated' => value is bool, 'examples' => value is List, + 'contentSchema' => value is bool || value is Map, _ => true, }; if (!valid) _fail(node, key, 'Invalid annotation value for "$key".'); + if (key == 'format') { + if (node.formatAssertion && + !_isSupportedJsonSchemaFormat(value as String)) { + _fail( + node, + key, + 'Unknown asserted format: $value.', + code: 'unsupported_format', + ); + } + node.format = value as String; + } + node.keywords[key] = value; + } else if (key == r'$vocabulary') { + if (value is! Map || + value.values.any((required) => required is! bool)) { + _fail(node, key, 'Expected a map of vocabulary URIs to booleans.'); + } + for (final entry in value.entries) { + if (entry.value == true && !knownVocabularies.contains(entry.key)) { + _fail( + node, + key, + 'Unknown required vocabulary: ${entry.key}.', + code: 'unsupported_vocabulary', + ); + } + } node.keywords[key] = value; - } else if (key == r'$ref') { + } else if (key == r'$ref' || key == r'$dynamicRef') { if (value is! String) _fail(node, key, 'Expected a URI string.'); final uri = _resolve(node, key, node.baseUri, value); final resource = resources[uri.removeFragment()]; @@ -465,43 +610,68 @@ final class _JsonSchemaCompiler { code: 'unresolved_reference', ); } - node.reference = target; - } else if (key == 'properties') { - node.properties = node.maps[key] = { + if (key == r'$ref') { + node.reference = target; + } else { + node.dynamicReference = target; + if (!fragment.startsWith('/') && + target.dynamicAnchorName == fragment) { + node.dynamicReferenceName = fragment; + } + } + } else if (key == 'properties' || + key == 'patternProperties' || + key == 'dependentSchemas') { + final compiledMap = { for (final name in (value as Map).keys) name: _child(node, '$key/${_importPointerToken(name)}'), }; - } else if (supportedChildKeywords.contains(key)) { - if ((key == 'items' && source.containsKey('prefixItems')) || - (key == 'additionalProperties' && - source.containsKey('patternProperties'))) { - _unsupported( - node, - key, - 'Cannot retain $key without its unsupported sibling.', - ); - } else { - final child = node.children[key] = _child(node, key); - switch (key) { - case 'items': - node.items = child; - case 'additionalProperties': - node.additionalProperties = child; - case 'not': - node.not = child; - case 'contains': - node.contains = child; - case 'propertyNames': - node.propertyNames = child; - case 'if': - node.ifNode = child; - case 'then': - node.thenNode = child; - case 'else': - node.elseNode = child; - } + node.maps[key] = compiledMap; + switch (key) { + case 'properties': + node.properties = compiledMap; + case 'dependentSchemas': + node.dependentSchemas = compiledMap; + case 'patternProperties': + final compiledPatterns = <(String, RegExp, _ImportedNode)>[]; + for (final entry in compiledMap.entries) { + try { + compiledPatterns.add(( + entry.key, + RegExp(entry.key, unicode: true), + entry.value, + )); + } on FormatException catch (e) { + _fail(node, key, 'Invalid pattern property: ${e.message}'); + } + } + node.patternProperties = compiledPatterns; + } + } else if (childKeywords.contains(key)) { + final child = node.children[key] = _child(node, key); + switch (key) { + case 'items': + node.items = child; + case 'additionalProperties': + node.additionalProperties = child; + case 'not': + node.not = child; + case 'contains': + node.contains = child; + case 'propertyNames': + node.propertyNames = child; + case 'unevaluatedProperties': + node.unevaluatedProperties = child; + case 'unevaluatedItems': + node.unevaluatedItems = child; + case 'if': + node.ifNode = child; + case 'then': + node.thenNode = child; + case 'else': + node.elseNode = child; } - } else if (supportedListKeywords.contains(key)) { + } else if (listKeywords.contains(key)) { final targets = node.lists[key] = [ for (var i = 0; i < (value as List).length; i++) _child(node, '$key/$i'), @@ -513,6 +683,8 @@ final class _JsonSchemaCompiler { node.allOf = targets; case 'oneOf': node.oneOf = targets; + case 'prefixItems': + node.prefixItems = targets; } } else if (key == 'type') { const types = { @@ -544,6 +716,21 @@ final class _JsonSchemaCompiler { } node.keywords[key] = value; node.required = List.unmodifiable(value); + } else if (key == 'dependentRequired') { + if (value is! Map || + value.values.any( + (names) => + names is! List || + names.any((name) => name is! String) || + names.toSet().length != names.length, + )) { + _fail(node, key, 'Expected a map of unique property-name lists.'); + } + node.keywords[key] = value; + node.dependentRequired = { + for (final entry in value.entries) + entry.key: List.unmodifiable(entry.value! as List), + }; } else if (key == 'enum') { if (value is! List) { _fail(node, key, 'Expected an enum array.'); @@ -561,7 +748,9 @@ final class _JsonSchemaCompiler { } else if (key == 'const') { node.keywords[key] = node.constValue = value; node.hasConst = true; - } else if (counts.contains(key)) { + } else if (counts.contains(key) || + key == 'minContains' || + key == 'maxContains') { if (value is! num || value < 0 || value % 1 != 0) { _fail(node, key, 'Expected a non-negative integer.'); } @@ -579,6 +768,10 @@ final class _JsonSchemaCompiler { node.minProperties = value; case 'maxProperties': node.maxProperties = value; + case 'minContains': + node.minContains = value; + case 'maxContains': + node.maxContains = value; } } else if (bounds.contains(key)) { if (value is! num) _fail(node, key, 'Expected a number.'); @@ -594,8 +787,8 @@ final class _JsonSchemaCompiler { node.exclusiveMaximum = value; } } else if (key == 'multipleOf') { - if (value is! num || value <= 0) { - _fail(node, key, 'Expected a positive number.'); + if (value is! num || !value.isFinite || value <= 0) { + _fail(node, key, 'Expected a positive finite number.'); } node.keywords[key] = node.multipleOf = value; } else if (key == 'uniqueItems') { @@ -611,19 +804,10 @@ final class _JsonSchemaCompiler { _fail(node, key, 'Invalid regular expression: ${e.message}'); } node.keywords[key] = value; - } else if (key == 'format') { - if (value is! String) { - _fail(node, key, 'Expected a format string.'); - } - if (!supportedFormats.contains(value)) { - _unsupported(node, key, 'Format "$value" is not supported.'); - continue; - } - node.keywords[key] = node.format = value; - } else if (key.startsWith('x-')) { - node.keywords[key] = value; } else { - _unsupported(node, key, 'Keyword "$key" is not supported.'); + // Unknown keywords are annotations in the standard 2020-12 dialect. + // Their JSON values survive export, but never assert against instances. + node.keywords[key] = value; } } } diff --git a/packages/ack/lib/src/json_schema/json_schema_formats.dart b/packages/ack/lib/src/json_schema/json_schema_formats.dart new file mode 100644 index 00000000..531abf19 --- /dev/null +++ b/packages/ack/lib/src/json_schema/json_schema_formats.dart @@ -0,0 +1,498 @@ +part of '../schemas/schema.dart'; + +const _jsonSchemaFormats = { + 'date', + 'time', + 'date-time', + 'duration', + 'email', + 'idn-email', + 'hostname', + 'idn-hostname', + 'ipv4', + 'ipv6', + 'uri', + 'uri-reference', + 'iri', + 'iri-reference', + 'uri-template', + 'json-pointer', + 'relative-json-pointer', + 'regex', + 'uuid', +}; + +bool _isSupportedJsonSchemaFormat(String format) => + _jsonSchemaFormats.contains(format); + +bool _isValidJsonSchemaFormat(String format, String value) => switch (format) { + 'date' => _isFullDate(value), + 'time' => _isFullTime(value), + 'date-time' => _isDateTime(value), + 'duration' => _isDuration(value), + 'email' => _isEmail(value, international: false), + 'idn-email' => _isEmail(value, international: true), + 'hostname' => _isHostname(value, international: false), + 'idn-hostname' => _isHostname(value, international: true), + 'ipv4' => _isIpv4(value), + 'ipv6' => _isIpv6(value), + 'uri' => _isUri(value, absolute: true, international: false), + 'uri-reference' => _isUri(value, absolute: false, international: false), + 'iri' => _isUri(value, absolute: true, international: true), + 'iri-reference' => _isUri(value, absolute: false, international: true), + 'uri-template' => _isUriTemplate(value), + 'json-pointer' => _isJsonPointer(value), + 'relative-json-pointer' => _isRelativeJsonPointer(value), + 'regex' => _isRegex(value), + 'uuid' => RegExp( + r'^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$', + ).hasMatch(value), + _ => false, +}; + +bool _isFullDate(String value) { + final match = RegExp(r'^([0-9]{4})-([0-9]{2})-([0-9]{2})$').firstMatch(value); + if (match == null) return false; + final year = int.parse(match[1]!); + final month = int.parse(match[2]!); + final day = int.parse(match[3]!); + if (year == 0 || month < 1 || month > 12 || day < 1) return false; + final leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + final days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + return day <= days[month - 1]; +} + +final _fullTimePattern = RegExp( + r'^([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.([0-9]+))?([Zz]|([+-])([0-9]{2}):([0-9]{2}))$', +); + +bool _isFullTime(String value) { + final match = _fullTimePattern.firstMatch(value); + if (match == null) return false; + final hour = int.parse(match[1]!); + final minute = int.parse(match[2]!); + final second = int.parse(match[3]!); + final offsetHour = match[7] == null ? 0 : int.parse(match[7]!); + final offsetMinute = match[8] == null ? 0 : int.parse(match[8]!); + if (hour > 23 || + minute > 59 || + second > 60 || + offsetHour > 23 || + offsetMinute > 59) { + return false; + } + if (second == 60) { + final signedOffset = + (offsetHour * 60 + offsetMinute) * (match[6] == '-' ? -1 : 1); + final utcMinute = (hour * 60 + minute - signedOffset) % (24 * 60); + if (utcMinute != 23 * 60 + 59) return false; + } + return true; +} + +bool _isDateTime(String value) { + if (value.length < 12 || (value[10] != 'T' && value[10] != 't')) { + return false; + } + return _isFullDate(value.substring(0, 10)) && + _isFullTime(value.substring(11)); +} + +bool _isDuration(String value) { + if (RegExp(r'^P[0-9]+W$').hasMatch(value)) return true; + final match = RegExp( + r'^P(?:([0-9]+)Y)?(?:([0-9]+)M)?(?:([0-9]+)D)?(?:T(?:([0-9]+)H)?(?:([0-9]+)M)?(?:([0-9]+)S)?)?$', + ).firstMatch(value); + if (match == null) return false; + final present = [for (var i = 1; i <= 6; i++) match[i] != null]; + if (!present.contains(true)) return false; + if (value.contains('T') && !present.sublist(3).contains(true)) return false; + if (present[0] && present[2] && !present[1]) return false; + if (present[3] && present[5] && !present[4]) return false; + return true; +} + +bool _isIpv4(String value) { + final parts = value.split('.'); + if (parts.length != 4) return false; + for (final part in parts) { + if (!RegExp(r'^(0|[1-9][0-9]{0,2})$').hasMatch(part)) return false; + if (int.parse(part) > 255) return false; + } + return true; +} + +bool _isIpv6(String value) { + // Uri.parseIPv6Address differs across supported Dart SDKs (notably for + // whitespace and IPv4 tails), so validate the RFC 3986 grammar ourselves. + if (value.isEmpty || RegExp(r'[^0-9A-Fa-f:.]').hasMatch(value)) { + return false; + } + var address = value; + var embeddedGroups = 0; + if (value.contains('.')) { + final separator = value.lastIndexOf(':'); + if (separator < 0) return false; + final tail = value.substring(separator + 1); + if (!_isIpv4(tail)) return false; + address = value.substring(0, separator); + if (address.endsWith(':')) address += ':'; + if (address.contains('.')) return false; + embeddedGroups = 2; + } + final compressed = address.contains('::'); + if (compressed) { + if (address.contains(':::') || + address.indexOf('::', address.indexOf('::') + 2) >= 0 || + (address.startsWith(':') && !address.startsWith('::')) || + (address.endsWith(':') && !address.endsWith('::'))) { + return false; + } + } else if (address.startsWith(':') || address.endsWith(':')) { + return false; + } + final groups = address.split(':').where((group) => group.isNotEmpty).toList(); + if (groups.any((group) => group.length > 4)) return false; + final expectedGroups = 8 - embeddedGroups; + return compressed + ? groups.length < expectedGroups + : groups.length == expectedGroups; +} + +bool _isJsonPointer(String value) { + if (value.isEmpty) return true; + if (!value.startsWith('/')) return false; + return !RegExp(r'~(?![01])').hasMatch(value); +} + +bool _isRelativeJsonPointer(String value) { + final match = RegExp(r'^(0|[1-9][0-9]*)(#|(?:/.*)?)$').firstMatch(value); + if (match == null) return false; + final suffix = match[2]!; + return suffix == '#' || _isJsonPointer(suffix); +} + +bool _isRegex(String value) { + try { + RegExp(value, unicode: true); + return true; + } on FormatException { + return false; + } +} + +bool _isHostname(String value, {required bool international}) { + if (!international && value.runes.any((rune) => rune > 127)) return false; + final mapped = international + ? unorm + .nfkc( + value.replaceAll(RegExp('[。.。]'), '.').replaceAll('\u200b', ''), + ) + .toLowerCase() + : value.toLowerCase(); + if (mapped.isEmpty) return false; + final asciiLabels = []; + final unicodeLabels = []; + for (final rawLabel in mapped.split('.')) { + if (rawLabel.isEmpty || + rawLabel.startsWith('-') || + rawLabel.endsWith('-')) { + return false; + } + if (!international && rawLabel.runes.any((rune) => rune > 127)) { + return false; + } + var label = rawLabel; + if (rawLabel.startsWith('xn--')) { + try { + label = punycoder.punycode.decode(rawLabel.substring(4)); + } on FormatException { + return false; + } + if (label.isEmpty || label.runes.every((rune) => rune < 128)) { + return false; + } + label = unorm.nfkc(label).toLowerCase(); + if (punycoder.punycode.encode(label).toLowerCase() != + rawLabel.substring(4)) { + return false; + } + } else if (rawLabel.length >= 4 && + rawLabel[2] == '-' && + rawLabel[3] == '-') { + return false; + } + if (!_isIdnaLabel(label)) return false; + final ascii = label.runes.every((rune) => rune < 128) + ? label + : 'xn--${punycoder.punycode.encode(label).toLowerCase()}'; + if (ascii.length > 63) return false; + asciiLabels.add(ascii); + unicodeLabels.add(label); + } + if (asciiLabels.join('.').length > 253) return false; + if (unicodeLabels.any((label) => label.runes.any(_isRtlRune))) { + for (final label in unicodeLabels) { + if (label.runes.any(_isRtlRune)) continue; + if (!RegExp(r'^[a-z]').hasMatch(label)) return false; + } + } + return true; +} + +bool _isIdnaLabel(String label) { + final runes = label.runes.toList(); + if (runes.isEmpty || + RegExp(r'^\p{M}', unicode: true).hasMatch(label) || + (label.length >= 4 && label[2] == '-' && label[3] == '-') || + label.startsWith('-') || + label.endsWith('-')) { + return false; + } + var hasArabicIndic = false; + var hasExtendedArabicIndic = false; + var hasJapanese = false; + var hasRtl = false; + var hasLatin = false; + var hasEuropeanDigit = false; + for (var i = 0; i < runes.length; i++) { + final rune = runes[i]; + if (rune < 128 && + !((rune >= 0x61 && rune <= 0x7a) || + (rune >= 0x30 && rune <= 0x39) || + rune == 0x2d)) { + return false; + } + if (rune >= 128 && + !RegExp( + r'^[\p{L}\p{M}\p{N}]$', + unicode: true, + ).hasMatch(String.fromCharCode(rune)) && + !const { + 0x00b7, + 0x0375, + 0x05f3, + 0x05f4, + 0x06fd, + 0x06fe, + 0x0f0b, + 0x200c, + 0x200d, + 0x30fb, + }.contains(rune)) { + return false; + } + if (rune <= 0x20 || + (rune >= 0x7f && rune <= 0x9f) || + rune == 0x302e || + rune == 0x302f || + (rune >= 0x3031 && rune <= 0x3035) || + rune == 0x303b || + rune == 0x0640 || + rune == 0x07fa) { + return false; + } + if (rune >= 0x61 && rune <= 0x7a) hasLatin = true; + if (rune >= 0x30 && rune <= 0x39) hasEuropeanDigit = true; + if (rune >= 0x0660 && rune <= 0x0669) hasArabicIndic = true; + if (rune >= 0x06f0 && rune <= 0x06f9) hasExtendedArabicIndic = true; + if (_isRtlRune(rune)) hasRtl = true; + if (rune != 0x30fb && _isJapaneseRune(rune)) hasJapanese = true; + if (rune == 0x00b7 && + (i == 0 || + i == runes.length - 1 || + runes[i - 1] != 0x6c || + runes[i + 1] != 0x6c)) { + return false; + } + if (rune == 0x0375 && + (i == runes.length - 1 || !_isGreekRune(runes[i + 1]))) { + return false; + } + if ((rune == 0x05f3 || rune == 0x05f4) && + (i == 0 || !_isHebrewRune(runes[i - 1]))) { + return false; + } + if ((rune == 0x200c || rune == 0x200d) && + (i == 0 || !_isVirama(runes[i - 1]))) { + if (rune == 0x200d || + i == 0 || + i == runes.length - 1 || + !_isArabicRune(runes[i - 1]) || + !_isArabicRune(runes[i + 1])) { + return false; + } + } + } + if (hasArabicIndic && hasExtendedArabicIndic) return false; + if (hasArabicIndic && !hasRtl && !hasLatin) return false; + if (runes.contains(0x30fb) && !hasJapanese) return false; + if (hasRtl) { + if (!_isRtlRune(runes.first) || + hasLatin || + (hasEuropeanDigit && (hasArabicIndic || hasExtendedArabicIndic))) { + return false; + } + } + return true; +} + +bool _isHebrewRune(int rune) => rune >= 0x0590 && rune <= 0x05ff; +bool _isArabicRune(int rune) => + rune >= 0x0600 && + rune <= 0x08ff && + !(rune >= 0x0660 && rune <= 0x0669) && + !(rune >= 0x06f0 && rune <= 0x06f9); +bool _isRtlRune(int rune) => _isHebrewRune(rune) || _isArabicRune(rune); +bool _isGreekRune(int rune) => + (rune >= 0x0370 && rune <= 0x03ff) || (rune >= 0x1f00 && rune <= 0x1fff); +bool _isJapaneseRune(int rune) => + (rune >= 0x3040 && rune <= 0x30ff) || (rune >= 0x3400 && rune <= 0x9fff); +bool _isVirama(int rune) => const { + 0x094d, + 0x09cd, + 0x0a4d, + 0x0acd, + 0x0b4d, + 0x0bcd, + 0x0c4d, + 0x0ccd, + 0x0d4d, + 0x0dca, + 0x0e3a, + 0x0f84, + 0x1039, + 0x1714, +}.contains(rune); + +bool _isEmail(String value, {required bool international}) { + final match = RegExp( + r'^("(?:[^"\\]|\\[\s\S])*"|[^@]+)@(.+)$', + ).firstMatch(value); + if (match == null) return false; + final local = match[1]!; + final domain = match[2]!; + if (!international && !local.runes.every((rune) => rune < 128)) { + return false; + } + if (local.startsWith('"')) { + if (!local.endsWith('"')) return false; + if (RegExp(r'[\u0000-\u001f\u007f]').hasMatch(local)) return false; + } else { + final atom = international + ? RegExp(r'^[^\s@\x00-\x20\x7f(),:;<>\[\]"\\]+$') + : RegExp(r"^[A-Za-z0-9!#\$%&'*+/=?^_`{|}~.-]+$"); + if (!atom.hasMatch(local) || + local.startsWith('.') || + local.endsWith('.') || + local.contains('..')) { + return false; + } + } + if (domain.startsWith('[') && domain.endsWith(']')) { + final literal = domain.substring(1, domain.length - 1); + if (literal.toLowerCase().startsWith('ipv6:')) { + return _isIpv6(literal.substring(5)); + } + return _isIpv4(literal); + } + return _isHostname(domain, international: international); +} + +bool _hasValidPercents(String value) { + for (var i = 0; i < value.length; i++) { + if (value[i] != '%') continue; + if (i + 2 >= value.length || + !RegExp(r'^[0-9a-fA-F]{2}$').hasMatch(value.substring(i + 1, i + 3))) { + return false; + } + i += 2; + } + return true; +} + +bool _isUri( + String value, { + required bool absolute, + required bool international, +}) { + if (!_hasValidPercents(value)) return false; + if (RegExp(r'[\u0000-\u0020\u007f<>"{}|\\^`]').hasMatch(value)) { + return false; + } + if (!international && value.runes.any((rune) => rune > 127)) return false; + if (absolute && !RegExp(r'^[A-Za-z][A-Za-z0-9+.-]*:').hasMatch(value)) { + return false; + } + if (!absolute && + RegExp(r'^[^/?.#][^/]*:').hasMatch(value) && + !RegExp(r'^[A-Za-z][A-Za-z0-9+.-]*:').hasMatch(value)) { + return false; + } + try { + final firstBracket = value.indexOf('['); + final lastBracket = value.lastIndexOf(']'); + if (firstBracket >= 0 || lastBracket >= 0) { + final authorityMarker = value.indexOf('//'); + if (authorityMarker < 0) return false; + final authorityStart = authorityMarker + 2; + var authorityEnd = value.length; + for (final delimiter in ['/', '?', '#']) { + final next = value.indexOf(delimiter, authorityStart); + if (next >= 0 && next < authorityEnd) authorityEnd = next; + } + if (firstBracket < authorityStart || + firstBracket >= authorityEnd || + lastBracket < firstBracket || + lastBracket >= authorityEnd) { + return false; + } + final literal = value.substring(firstBracket + 1, lastBracket); + if (!_isIpv6(literal) && + !RegExp( + r"^[vV][0-9A-Fa-f]+\.[A-Za-z0-9._~!$&'()*+,;=:-]+$", + ).hasMatch(literal)) { + return false; + } + } + final uri = Uri.parse(value); + if (absolute && !uri.hasScheme) return false; + if (uri.path.contains('[') || uri.path.contains(']')) return false; + if (uri.hasAuthority && + uri.host.contains(':') && + !value.contains('[${uri.host}]')) { + return false; + } + return true; + } on FormatException { + return false; + } +} + +bool _isUriTemplate(String value) { + var index = 0; + while (index < value.length) { + final char = value[index]; + if (char == '}') return false; + if (char != '{') { + index++; + continue; + } + final end = value.indexOf('}', index + 1); + if (end < 0) return false; + var expression = value.substring(index + 1, end); + if (expression.isEmpty || expression.contains('{')) return false; + if ('+#./;?&'.contains(expression[0])) expression = expression.substring(1); + if (expression.isEmpty) return false; + for (final variable in expression.split(',')) { + if (variable.isEmpty) return false; + final match = RegExp( + r'^([A-Za-z0-9_]|%[0-9A-Fa-f]{2})+(?:\.(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})+)*(?:\*|:[1-9][0-9]{0,3})?$', + ).firstMatch(variable); + if (match == null) return false; + } + index = end + 1; + } + return _hasValidPercents(value.replaceAll(RegExp(r'\{[^{}]*\}'), '')) && + !RegExp(r'[\u0000-\u0020\u007f]').hasMatch(value); +} diff --git a/packages/ack/lib/src/json_schema/standard_meta_schemas.dart b/packages/ack/lib/src/json_schema/standard_meta_schemas.dart new file mode 100644 index 00000000..54e1dd51 --- /dev/null +++ b/packages/ack/lib/src/json_schema/standard_meta_schemas.dart @@ -0,0 +1,358 @@ +// Standard draft 2020-12 meta-schemas bundled for offline VM/web use. +// Source: https://json-schema.org/draft/2020-12/schema and its meta/* refs. +// Do not edit these upstream JSON documents by hand. +part of '../schemas/schema.dart'; + +const _standardMetaSchemaJson = { + 'https://json-schema.org/draft/2020-12/schema': r''' +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/schema", + "$vocabulary": { + "https://json-schema.org/draft/2020-12/vocab/core": true, + "https://json-schema.org/draft/2020-12/vocab/applicator": true, + "https://json-schema.org/draft/2020-12/vocab/unevaluated": true, + "https://json-schema.org/draft/2020-12/vocab/validation": true, + "https://json-schema.org/draft/2020-12/vocab/meta-data": true, + "https://json-schema.org/draft/2020-12/vocab/format-annotation": true, + "https://json-schema.org/draft/2020-12/vocab/content": true + }, + "$dynamicAnchor": "meta", + + "title": "Core and Validation specifications meta-schema", + "allOf": [ + {"$ref": "meta/core"}, + {"$ref": "meta/applicator"}, + {"$ref": "meta/unevaluated"}, + {"$ref": "meta/validation"}, + {"$ref": "meta/meta-data"}, + {"$ref": "meta/format-annotation"}, + {"$ref": "meta/content"} + ], + "type": ["object", "boolean"], + "$comment": "This meta-schema also defines keywords that have appeared in previous drafts in order to prevent incompatible extensions as they remain in common use.", + "properties": { + "definitions": { + "$comment": "\"definitions\" has been replaced by \"$defs\".", + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "deprecated": true, + "default": {} + }, + "dependencies": { + "$comment": "\"dependencies\" has been split and replaced by \"dependentSchemas\" and \"dependentRequired\" in order to serve their differing semantics.", + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$dynamicRef": "#meta" }, + { "$ref": "meta/validation#/$defs/stringArray" } + ] + }, + "deprecated": true, + "default": {} + }, + "$recursiveAnchor": { + "$comment": "\"$recursiveAnchor\" has been replaced by \"$dynamicAnchor\".", + "$ref": "meta/core#/$defs/anchorString", + "deprecated": true + }, + "$recursiveRef": { + "$comment": "\"$recursiveRef\" has been replaced by \"$dynamicRef\".", + "$ref": "meta/core#/$defs/uriReferenceString", + "deprecated": true + } + } +} +''', + 'https://json-schema.org/draft/2020-12/meta/core': r''' +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/core", + "$dynamicAnchor": "meta", + + "title": "Core vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "$id": { + "$ref": "#/$defs/uriReferenceString", + "$comment": "Non-empty fragments not allowed.", + "pattern": "^[^#]*#?$" + }, + "$schema": { "$ref": "#/$defs/uriString" }, + "$ref": { "$ref": "#/$defs/uriReferenceString" }, + "$anchor": { "$ref": "#/$defs/anchorString" }, + "$dynamicRef": { "$ref": "#/$defs/uriReferenceString" }, + "$dynamicAnchor": { "$ref": "#/$defs/anchorString" }, + "$vocabulary": { + "type": "object", + "propertyNames": { "$ref": "#/$defs/uriString" }, + "additionalProperties": { + "type": "boolean" + } + }, + "$comment": { + "type": "string" + }, + "$defs": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" } + } + }, + "$defs": { + "anchorString": { + "type": "string", + "pattern": "^[A-Za-z_][-A-Za-z0-9._]*$" + }, + "uriString": { + "type": "string", + "format": "uri" + }, + "uriReferenceString": { + "type": "string", + "format": "uri-reference" + } + } +} +''', + 'https://json-schema.org/draft/2020-12/meta/applicator': r''' +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/applicator", + "$dynamicAnchor": "meta", + + "title": "Applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "prefixItems": { "$ref": "#/$defs/schemaArray" }, + "items": { "$dynamicRef": "#meta" }, + "contains": { "$dynamicRef": "#meta" }, + "additionalProperties": { "$dynamicRef": "#meta" }, + "properties": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependentSchemas": { + "type": "object", + "additionalProperties": { "$dynamicRef": "#meta" }, + "default": {} + }, + "propertyNames": { "$dynamicRef": "#meta" }, + "if": { "$dynamicRef": "#meta" }, + "then": { "$dynamicRef": "#meta" }, + "else": { "$dynamicRef": "#meta" }, + "allOf": { "$ref": "#/$defs/schemaArray" }, + "anyOf": { "$ref": "#/$defs/schemaArray" }, + "oneOf": { "$ref": "#/$defs/schemaArray" }, + "not": { "$dynamicRef": "#meta" } + }, + "$defs": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$dynamicRef": "#meta" } + } + } +} +''', + 'https://json-schema.org/draft/2020-12/meta/unevaluated': r''' +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/unevaluated", + "$dynamicAnchor": "meta", + + "title": "Unevaluated applicator vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "unevaluatedItems": { "$dynamicRef": "#meta" }, + "unevaluatedProperties": { "$dynamicRef": "#meta" } + } +} +''', + 'https://json-schema.org/draft/2020-12/meta/validation': r''' +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/validation", + "$dynamicAnchor": "meta", + + "title": "Validation vocabulary meta-schema", + "type": ["object", "boolean"], + "properties": { + "type": { + "anyOf": [ + { "$ref": "#/$defs/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/$defs/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "const": true, + "enum": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/$defs/nonNegativeInteger" }, + "minLength": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "maxItems": { "$ref": "#/$defs/nonNegativeInteger" }, + "minItems": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "maxContains": { "$ref": "#/$defs/nonNegativeInteger" }, + "minContains": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 1 + }, + "maxProperties": { "$ref": "#/$defs/nonNegativeInteger" }, + "minProperties": { "$ref": "#/$defs/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/$defs/stringArray" }, + "dependentRequired": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/stringArray" + } + } + }, + "$defs": { + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "$ref": "#/$defs/nonNegativeInteger", + "default": 0 + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + } +} +''', + 'https://json-schema.org/draft/2020-12/meta/meta-data': r''' +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/meta-data", + "$dynamicAnchor": "meta", + + "title": "Meta-data vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "deprecated": { + "type": "boolean", + "default": false + }, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + } + } +} +''', + 'https://json-schema.org/draft/2020-12/meta/format-annotation': r''' +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/format-annotation", + "$dynamicAnchor": "meta", + + "title": "Format vocabulary meta-schema for annotation results", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} +''', + 'https://json-schema.org/draft/2020-12/meta/format-assertion': r''' +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/format-assertion", + "$dynamicAnchor": "meta", + + "title": "Format vocabulary meta-schema for assertion results", + "type": ["object", "boolean"], + "properties": { + "format": { "type": "string" } + } +} +''', + 'https://json-schema.org/draft/2020-12/meta/content': r''' +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://json-schema.org/draft/2020-12/meta/content", + "$dynamicAnchor": "meta", + + "title": "Content vocabulary meta-schema", + + "type": ["object", "boolean"], + "properties": { + "contentEncoding": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentSchema": { "$dynamicRef": "#meta" } + } +} +''', +}; + +final _standardMetaSchemaDocuments = { + for (final entry in _standardMetaSchemaJson.entries) + Uri.parse(entry.key): jsonDecode(entry.value)!, +}; diff --git a/packages/ack/lib/src/schema_model/ack_schema_model.dart b/packages/ack/lib/src/schema_model/ack_schema_model.dart index 25893fee..8a54c88f 100644 --- a/packages/ack/lib/src/schema_model/ack_schema_model.dart +++ b/packages/ack/lib/src/schema_model/ack_schema_model.dart @@ -241,6 +241,10 @@ sealed class AckSchemaModel { keywords, commonHandled, ), + AckRawSchemaModel schema => schema._withJsonSchemaKeywords( + keywords, + commonHandled, + ), AckNullSchemaModel schema => schema._withJsonSchemaKeywords( keywords, commonHandled, @@ -328,6 +332,133 @@ sealed class AckSchemaModel { } } +/// A precompiled JSON Schema resource whose 2020-12 keywords must remain +/// attached to their original resource instead of being lowered to Draft 7. +final class AckRawSchemaModel extends AckSchemaModel { + const AckRawSchemaModel({ + required this.schema, + super.title, + super.description, + super.nullable, + super.defaultValue, + super.warnings, + super.extensions, + }); + + AckRawSchemaModel._(_AckSchemaModelCommon common, {required this.schema}) + : super._(common); + + final Map schema; + + @override + Map toJsonSchema() { + final body = {...schema, ..._common.toJson()}; + if (!nullable) return body; + const resourceKeywords = {r'$schema', r'$id', r'$defs', 'definitions'}; + final relocated = _relocateRootPointers( + body, + Uri.parse(body[r'$id'] as String), + resourceKeywords, + ); + final resource = { + for (final entry in relocated.entries) + if (resourceKeywords.contains(entry.key)) entry.key: entry.value, + }; + final assertion = { + for (final entry in relocated.entries) + if (!resourceKeywords.contains(entry.key)) entry.key: entry.value, + }; + return { + ...resource, + 'anyOf': [assertion, _nullSchemaJson], + }; + } + + @override + AckRawSchemaModel _rebuildWithCommon(_AckSchemaModelCommon common) => + AckRawSchemaModel._(common, schema: schema); + + AckSchemaModel _withJsonSchemaKeywords( + Map keywords, + Set commonHandled, + ) => _withUnhandledKeywords(keywords, commonHandled); +} + +Map _relocateRootPointers( + Map source, + Uri rootUri, + Set retainedKeywords, +) { + const mapKeywords = { + r'$defs', + 'definitions', + 'properties', + 'patternProperties', + 'dependentSchemas', + }; + const childKeywords = { + 'items', + 'additionalProperties', + 'not', + 'contains', + 'propertyNames', + 'if', + 'then', + 'else', + 'unevaluatedProperties', + 'unevaluatedItems', + }; + const listKeywords = {'anyOf', 'allOf', 'oneOf', 'prefixItems'}; + + Object? visit(Object? value, Uri base) { + if (value is! Map) return value; + final declaredId = value[r'$id']; + final currentBase = declaredId is String + ? base.resolve(declaredId).removeFragment() + : base; + final rewritten = Map.from(value); + for (final key in [r'$ref', r'$dynamicRef']) { + final reference = rewritten[key]; + if (reference is! String) continue; + final resolved = currentBase.resolve(reference); + final fragment = resolved.fragment; + if (resolved.removeFragment() != rootUri) continue; + if (fragment.isEmpty) { + rewritten[key] = rootUri.replace(fragment: '/anyOf/0').toString(); + continue; + } + if (!fragment.startsWith('/')) continue; + final first = fragment.substring(1).split('/').first; + if (retainedKeywords.contains(first)) continue; + rewritten[key] = rootUri + .replace(fragment: '/anyOf/0$fragment') + .toString(); + } + for (final key in mapKeywords) { + if (rewritten[key] case final Map children) { + rewritten[key] = children.map( + (name, child) => MapEntry(name, visit(child, currentBase)), + ); + } + } + for (final key in childKeywords) { + if (rewritten.containsKey(key)) { + rewritten[key] = visit(rewritten[key], currentBase); + } + } + for (final key in listKeywords) { + if (rewritten[key] case final List children) { + rewritten[key] = [ + for (final child in children) visit(child, currentBase), + ]; + } + } + return rewritten; + } + + return visit(source, rootUri)! as Map; +} + final class AckRefSchemaModel extends AckSchemaModel { const AckRefSchemaModel({ required this.refName, diff --git a/packages/ack/lib/src/schema_model/ack_schema_model_builder.dart b/packages/ack/lib/src/schema_model/ack_schema_model_builder.dart index f3f95df6..a2bfa4a9 100644 --- a/packages/ack/lib/src/schema_model/ack_schema_model_builder.dart +++ b/packages/ack/lib/src/schema_model/ack_schema_model_builder.dart @@ -17,16 +17,31 @@ extension AckSchemaModelExtension< Runtime extends Object > on AckSchema { - AckSchemaModel toSchemaModel() => _SchemaModelBuilder().build(this); + /// Preserves imported Draft 2020-12 resources, including dynamic references. + /// Native Ack schemas continue to use their Draft-7 representation. + AckSchemaModel toSchemaModel() => + _SchemaModelBuilder(preserveImportedDialect: true).build(this); + + /// Lowers imported schemas into the legacy Draft-7 `definitions` / `$ref` + /// representation when possible. + AckSchemaModel toSchemaModelDraft7() => + _SchemaModelBuilder(preserveImportedDialect: false).build(this); + + /// Exports imported schemas using the legacy Draft-7 `definitions` / `$ref` + /// representation when possible. + Map toJsonSchemaDraft7() => + toSchemaModelDraft7().toJsonSchema(); } final class _SchemaModelBuilder { + _SchemaModelBuilder({this.preserveImportedDialect = true}); + + final bool preserveImportedDialect; // Every emitted definition name is reserved here. A null value marks a lazy // target that is currently being built. final _definitions = {}; - // Lazy-target identity is tracked separately because imported definitions - // are complete schema bodies, not recursive lazy targets. + // Lazy-target identity is tracked separately from emitted definitions. final _lazyTargets = {}; var _importCount = 0; @@ -161,7 +176,17 @@ final class _SchemaModelBuilder { } AckSchemaModel _imported(ImportedJsonSchema schema) { - final prefix = '_ack_import_${_importCount++}_'; + final importIndex = _importCount++; + if (preserveImportedDialect) { + return AckRawSchemaModel( + schema: schema.export2020Document(importIndex: importIndex), + description: schema.description, + nullable: + schema.isNullable && + (!schema.sourceAllowsNull || schema.constraints.isNotEmpty), + ); + } + final prefix = '_ack_import_${importIndex}_'; for (final entry in schema.exportDefinitions(prefix).entries) { if (_definitions.containsKey(entry.key)) { throw ArgumentError( diff --git a/packages/ack/lib/src/schemas/imported_json_schema.dart b/packages/ack/lib/src/schemas/imported_json_schema.dart index 633b88be..7925711b 100644 --- a/packages/ack/lib/src/schemas/imported_json_schema.dart +++ b/packages/ack/lib/src/schemas/imported_json_schema.dart @@ -14,33 +14,167 @@ final class ImportedJsonSchema extends AckSchema Object document, { Uri? baseUri, Map documents = const {}, + bool assertFormats = false, }) { - final compiler = _JsonSchemaCompiler(); + final compiler = _JsonSchemaCompiler(assertFormats: assertFormats); final base = baseUri ?? Uri.parse('ack-import:///root.json'); final root = compiler.addDocument(document, base); + final externalRoots = <_ImportedNode>[]; for (final entry in documents.entries) { - compiler.addDocument(entry.value, base.resolveUri(entry.key)); + externalRoots.add( + compiler.addDocument(entry.value, base.resolveUri(entry.key)), + ); + } + for (final entry in _standardMetaSchemaDocuments.entries) { + if (!compiler.resources.containsKey(entry.key)) { + compiler.addDocument(entry.value, entry.key); + } } compiler.compile(root); if (compiler.diagnostics.isNotEmpty) { throw JsonSchemaImportException._(compiler.diagnostics); } - return ImportedJsonSchema._(root); + return ImportedJsonSchema._( + root, + externalRoots: externalRoots, + assertFormats: assertFormats, + ); } ImportedJsonSchema._( this._root, { + required List<_ImportedNode> externalRoots, + required bool assertFormats, bool? isNullable, super.isOptional, String? description, super.constraints, super.refinements, - }) : super( + }) : _externalRoots = externalRoots, + _assertFormats = assertFormats, + super( isNullable: isNullable ?? _checkImportedNode(_root, null) == null, description: description ?? _root.keywords['description'] as String?, ); final _ImportedNode _root; + final List<_ImportedNode> _externalRoots; + final bool _assertFormats; + + static final _assertionDialect = Uri.parse( + 'urn:ack:json-schema:2020-12:format-assertion', + ); + + /// Keeps the documented 1.x Draft-7 definitions/ref export for imports. + @internal + Map> exportDefinitions(String prefix) { + final names = <_ImportedNode, String>{}; + final refs = <_ImportedNode, String>{}; + void visit(_ImportedNode node) { + if (names.containsKey(node)) return; + final name = '$prefix${names.length}'; + names[node] = name; + refs[node] = '#/definitions/${_importPointerToken(name)}'; + for (final child in node.dependencies) { + visit(child); + } + } + + visit(_root); + return { + for (final entry in names.entries) + entry.value: entry.key.renderLegacy((node) => refs[node]!), + }; + } + + /// Exports the original resources with 2020-12 identity and reference + /// semantics intact. Supplied documents are embedded as distinct resources. + @internal + Map export2020Document({required int importIndex}) { + Map resource(_ImportedNode node, {Uri? syntheticId}) { + final source = cloneDefault(node.source); + return { + if (source is Map) ...source, + r'$id': (syntheticId ?? node.baseUri).toString(), + if (_assertFormats && + node.dialectUri == _JsonSchemaCompiler.standardDialect) + r'$schema': _assertionDialect.toString(), + if (source is bool) 'allOf': [source], + }; + } + + final syntheticId = + _root.documentUri.toString() == 'ack-import:///root.json' && + !(_root.source is Map && + (_root.source as Map).containsKey(r'$id')) + ? Uri.parse('ack-import:///root-$importIndex.json') + : null; + final root = resource(_root, syntheticId: syntheticId); + root.putIfAbsent( + r'$schema', + () => _JsonSchemaCompiler.standardDialect.toString(), + ); + if (_externalRoots.isNotEmpty || _assertFormats) { + final definitions = { + ...?root[r'$defs'] as Map?, + }; + var index = 0; + var aliasIndex = 0; + for (final external in _externalRoots) { + while (definitions.containsKey('_ack_document_$index')) { + index++; + } + final name = '_ack_document_${index++}'; + definitions[name] = resource(external); + if (external.documentUri != external.baseUri) { + while (definitions.containsKey('_ack_retrieval_$aliasIndex')) { + aliasIndex++; + } + definitions['_ack_retrieval_${aliasIndex++}'] = { + r'$id': external.documentUri.toString(), + r'$ref': external.baseUri.toString(), + }; + } + } + if (_assertFormats) { + var name = '_ack_format_assertion_dialect'; + while (definitions.containsKey(name)) { + name = '${name}_'; + } + definitions[name] = { + r'$id': _assertionDialect.toString(), + r'$schema': _JsonSchemaCompiler.standardDialect.toString(), + r'$vocabulary': { + for (final uri in _JsonSchemaCompiler.knownVocabularies) uri: true, + }, + r'$dynamicAnchor': 'meta', + 'allOf': [ + for (final name in [ + 'core', + 'applicator', + 'unevaluated', + 'validation', + 'meta-data', + 'format-annotation', + 'format-assertion', + 'content', + ]) + {r'$ref': 'https://json-schema.org/draft/2020-12/meta/$name'}, + ], + }; + } + root[r'$defs'] = definitions; + } + if (!isNullable && sourceAllowsNull) { + root['allOf'] = [ + ...?root['allOf'] as List?, + const { + 'not': {'type': 'null'}, + }, + ]; + } + return root; + } /// Whether the source document itself accepts `null`, before fluent /// nullability overrides are applied. @@ -118,27 +252,6 @@ final class ImportedJsonSchema extends AckSchema return applyConstraintsAndRefinements(value, context); } - /// Builds root-scoped definitions for the shared schema-model renderer. - @internal - Map> exportDefinitions(String prefix) { - final refs = <_ImportedNode, String>{}; - void visit(_ImportedNode node) { - if (refs.containsKey(node)) return; - final name = '$prefix${refs.length}'; - refs[node] = '#/definitions/${_importPointerToken(name)}'; - for (final child in node.dependencies) { - visit(child); - } - } - - visit(_root); - String refOf(_ImportedNode node) => refs[node]!; - return { - for (final (i, node) in refs.keys.indexed) - '$prefix$i': node.render(refOf), - }; - } - @override ImportedJsonSchema copyWith({ bool? isNullable, @@ -148,6 +261,8 @@ final class ImportedJsonSchema extends AckSchema List>? refinements, }) => ImportedJsonSchema._( _root, + externalRoots: _externalRoots, + assertFormats: _assertFormats, isNullable: isNullable ?? this.isNullable, isOptional: isOptional ?? this.isOptional, description: description ?? this.description, @@ -206,163 +321,358 @@ final class _ImportViolation { ); } -_ImportViolation? _checkImportedNode(_ImportedNode node, Object? value) { - _ImportViolation fail(String keyword) => _ImportViolation(node, keyword); - if (node.isFalse) return fail(''); - if (node.types case final types?) { - if (!types.any((t) => _isImportType(t, value))) return fail('type'); - } - if (node.hasConst && !deepEquals(value, node.constValue)) { - return fail('const'); +final class _ImportEvaluation { + _ImportEvaluation([this.error]); + + final _ImportViolation? error; + final properties = {}; + final items = {}; + + void include(_ImportEvaluation other) { + properties.addAll(other.properties); + items.addAll(other.items); } - if (node.enumValues case final values?) { - if (!values.any((v) => deepEquals(v, value))) return fail('enum'); +} + +_ImportViolation? _checkImportedNode(_ImportedNode node, Object? value) => + _evaluateImportedNode(node, value).error; + +_ImportEvaluation _evaluateImportedNode( + _ImportedNode node, + Object? value, [ + List<_ImportedNode> scope = const [], +]) { + _ImportEvaluation fail(String keyword) => + _ImportEvaluation(_ImportViolation(node, keyword)); + _ImportEvaluation failAt(String keyword, String segment) => + _ImportEvaluation(_ImportViolation(node, keyword).at(segment)); + final evaluated = _ImportEvaluation(); + final resource = node.resourceRoot; + final currentScope = scope.isNotEmpty && identical(scope.last, resource) + ? scope + : [...scope, resource]; + if (node.isFalse) return fail(''); + if (node.validationVocabulary) { + if (node.types case final types?) { + if (!types.any((t) => _isImportType(t, value))) return fail('type'); + } + if (node.hasConst && !deepEquals(value, node.constValue)) { + return fail('const'); + } + if (node.enumValues case final values?) { + if (!values.any((v) => deepEquals(v, value))) return fail('enum'); + } } if (node.reference case final target?) { - final error = _checkImportedNode(target, value); - if (error != null) return error.via(const [r'$ref']); + final result = _evaluateImportedNode(target, value, currentScope); + if (result.error case final error?) { + return _ImportEvaluation(error.via(const [r'$ref'])); + } + evaluated.include(result); + } + if (node.dynamicReference case final staticTarget?) { + var target = staticTarget; + if (node.dynamicReferenceName case final name?) { + for (final resource in currentScope) { + if (resource.dynamicAnchors[name] case final override?) { + target = override; + break; + } + } + } + final result = _evaluateImportedNode(target, value, currentScope); + if (result.error case final error?) { + return _ImportEvaluation(error.via(const [r'$dynamicRef'])); + } + evaluated.include(result); } - if (node.allOf case final targets?) { - for (final (i, target) in targets.indexed) { - final error = _checkImportedNode(target, value); - if (error != null) return error.via(['allOf', '$i']); + if (node.allOf case final branches?) { + for (var i = 0; i < branches.length; i++) { + final result = _evaluateImportedNode(branches[i], value, currentScope); + if (result.error case final error?) { + return _ImportEvaluation(error.via(['allOf', '$i'])); + } + evaluated.include(result); } } if (node.anyOf case final branches?) { - if (!branches.any((n) => _checkImportedNode(n, value) == null)) { - return fail('anyOf'); + var success = false; + for (final branch in branches) { + final result = _evaluateImportedNode(branch, value, currentScope); + if (result.error == null) { + success = true; + evaluated.include(result); + } } + if (!success) return fail('anyOf'); } if (node.oneOf case final branches?) { - var matches = 0; + var successes = 0; + _ImportEvaluation? matched; for (final branch in branches) { - if (_checkImportedNode(branch, value) == null && ++matches > 1) break; + final result = _evaluateImportedNode(branch, value, currentScope); + if (result.error == null) { + successes++; + matched = result; + } } - if (matches != 1) return fail('oneOf'); + if (successes != 1) return fail('oneOf'); + evaluated.include(matched!); } if (node.not case final target?) { - if (_checkImportedNode(target, value) == null) return fail('not'); + if (_evaluateImportedNode(target, value, currentScope).error == null) { + return fail('not'); + } } if (node.ifNode case final condition?) { - final passed = _checkImportedNode(condition, value) == null; + final conditionResult = _evaluateImportedNode( + condition, + value, + currentScope, + ); + final passed = conditionResult.error == null; + if (passed) evaluated.include(conditionResult); final branch = passed ? node.thenNode : node.elseNode; if (branch != null) { - final error = _checkImportedNode(branch, value); - if (error != null) return error.via([passed ? 'then' : 'else']); + final result = _evaluateImportedNode(branch, value, currentScope); + if (result.error case final error?) { + return _ImportEvaluation(error.via([passed ? 'then' : 'else'])); + } + evaluated.include(result); } } switch (value) { case num(): - if (node.minimum case final limit? when value < limit) { - return fail('minimum'); - } - if (node.maximum case final limit? when value > limit) { - return fail('maximum'); - } - if (node.exclusiveMinimum case final limit? when value <= limit) { - return fail('exclusiveMinimum'); - } - if (node.exclusiveMaximum case final limit? when value >= limit) { - return fail('exclusiveMaximum'); + if (node.validationVocabulary) { + if (node.multipleOf case final divisor? + when !isExactDecimalMultipleOf(value, divisor)) { + return fail('multipleOf'); + } + if (node.minimum case final limit? when value < limit) { + return fail('minimum'); + } + if (node.maximum case final limit? when value > limit) { + return fail('maximum'); + } + if (node.exclusiveMinimum case final limit? when value <= limit) { + return fail('exclusiveMinimum'); + } + if (node.exclusiveMaximum case final limit? when value >= limit) { + return fail('exclusiveMaximum'); + } } - if (node.multipleOf case final divisor? - when !isExactDecimalMultipleOf(value, divisor)) { - return fail('multipleOf'); + case String(): + if (node.validationVocabulary) { + // Counting code points allocates, so only do it when a bound exists. + if (node.minLength != null || node.maxLength != null) { + final keyword = _sizeViolation( + value.runes.length, + node.minLength, + node.maxLength, + 'minLength', + 'maxLength', + ); + if (keyword != null) return fail(keyword); + } + if (node.pattern case final pattern? when !pattern.hasMatch(value)) { + return fail('pattern'); + } } - if (node.format case final format? - when !_isValidImportedNumericFormat(format, value)) { - return fail('format'); + if (node.formatAssertion) { + if (node.format case final format? + when !_isValidJsonSchemaFormat(format, value)) { + return fail('format'); + } } - case String(): - // Counting code points allocates, so only do it when a bound exists. - if (node.minLength != null || node.maxLength != null) { + case Map(): + if (node.validationVocabulary) { final keyword = _sizeViolation( - value.runes.length, - node.minLength, - node.maxLength, - 'minLength', - 'maxLength', + value.length, + node.minProperties, + node.maxProperties, + 'minProperties', + 'maxProperties', ); if (keyword != null) return fail(keyword); - } - if (node.format case final format? - when !_isValidImportedStringFormat(format, value)) { - return fail('format'); - } - if (node.pattern case final pattern? when !pattern.hasMatch(value)) { - return fail('pattern'); - } - case Map(): - final keyword = _sizeViolation( - value.length, - node.minProperties, - node.maxProperties, - 'minProperties', - 'maxProperties', - ); - if (keyword != null) return fail(keyword); - if (node.required case final required?) { - for (final key in required) { - if (!value.containsKey(key)) return fail('required').at(key); + if (node.required case final required?) { + for (final key in required) { + if (!value.containsKey(key)) return failAt('required', key); + } + } + if (node.dependentRequired case final requirements?) { + for (final entry in requirements.entries) { + if (!value.containsKey(entry.key)) continue; + for (final dependent in entry.value) { + if (!value.containsKey(dependent)) { + return failAt('dependentRequired', dependent); + } + } + } } } final properties = node.properties; + final patterns = node.patternProperties; final additional = node.additionalProperties; - if (properties != null || additional != null) { + if (properties != null || patterns != null || additional != null) { for (final MapEntry(:key, value: item) in value.entries) { - final declared = properties?[key]; - final target = declared ?? additional; - if (target == null) continue; - final error = _checkImportedNode(target, item); - if (error != null) { - return error.at( - key as String, - declared == null - ? const ['additionalProperties'] - : ['properties', key], + final propKey = key as String; + var matched = false; + if (properties?[propKey] case final target?) { + matched = true; + final result = _evaluateImportedNode(target, item, currentScope); + if (result.error case final error?) { + return _ImportEvaluation( + error.at(propKey, ['properties', propKey]), + ); + } + evaluated.properties.add(propKey); + } + if (patterns != null) { + for (final (rawPattern, regex, target) in patterns) { + if (!regex.hasMatch(propKey)) continue; + matched = true; + final result = _evaluateImportedNode(target, item, currentScope); + if (result.error case final error?) { + return _ImportEvaluation( + error.at(propKey, ['patternProperties', rawPattern]), + ); + } + evaluated.properties.add(propKey); + } + } + if (!matched && additional != null) { + final result = _evaluateImportedNode( + additional, + item, + currentScope, ); + if (result.error case final error?) { + return _ImportEvaluation( + error.at(propKey, const ['additionalProperties']), + ); + } + evaluated.properties.add(propKey); } } } + if (node.dependentSchemas case final dependencies?) { + for (final dependency in dependencies.entries) { + if (!value.containsKey(dependency.key)) continue; + final result = _evaluateImportedNode( + dependency.value, + value, + currentScope, + ); + if (result.error case final error?) { + return _ImportEvaluation( + error.via(['dependentSchemas', dependency.key]), + ); + } + evaluated.include(result); + } + } if (node.propertyNames case final target?) { for (final key in value.keys) { - final error = _checkImportedNode(target, key); - if (error != null) { - return error.atPropertyName(key as String, const ['propertyNames']); + final result = _evaluateImportedNode(target, key, currentScope); + if (result.error case final error?) { + return _ImportEvaluation( + error.atPropertyName(key as String, const ['propertyNames']), + ); } } } + if (node.unevaluatedProperties case final target?) { + for (final entry in value.entries) { + final key = entry.key as String; + if (evaluated.properties.contains(key)) continue; + final result = _evaluateImportedNode( + target, + entry.value, + currentScope, + ); + if (result.error case final error?) { + return _ImportEvaluation( + error.at(key, const ['unevaluatedProperties']), + ); + } + evaluated.properties.add(key); + } + } case List(): - final keyword = _sizeViolation( - value.length, - node.minItems, - node.maxItems, - 'minItems', - 'maxItems', - ); - if (keyword != null) return fail(keyword); - if (node.uniqueItems) { - for (var i = 0; i < value.length; i++) { - for (var j = 0; j < i; j++) { - if (deepEquals(value[i], value[j])) { - return fail('uniqueItems'); + if (node.validationVocabulary) { + final keyword = _sizeViolation( + value.length, + node.minItems, + node.maxItems, + 'minItems', + 'maxItems', + ); + if (keyword != null) return fail(keyword); + if (node.uniqueItems) { + for (var i = 0; i < value.length; i++) { + for (var j = 0; j < i; j++) { + if (deepEquals(value[i], value[j])) { + return fail('uniqueItems'); + } } } } } - if (node.contains case final target?) { - if (!value.any((item) => _checkImportedNode(target, item) == null)) { - return fail('contains'); + final prefixItems = node.prefixItems ?? const <_ImportedNode>[]; + for (var i = 0; i < prefixItems.length && i < value.length; i++) { + final result = _evaluateImportedNode( + prefixItems[i], + value[i], + currentScope, + ); + if (result.error case final error?) { + return _ImportEvaluation(error.at('$i', ['prefixItems', '$i'])); } + evaluated.items.add(i); } if (node.items case final target?) { + for (var i = prefixItems.length; i < value.length; i++) { + final result = _evaluateImportedNode(target, value[i], currentScope); + if (result.error case final error?) { + return _ImportEvaluation(error.at('$i', const ['items'])); + } + evaluated.items.add(i); + } + } + if (node.contains case final target?) { + var matches = 0; + for (var i = 0; i < value.length; i++) { + if (_evaluateImportedNode(target, value[i], currentScope).error == + null) { + matches++; + evaluated.items.add(i); + } + } + final minimum = node.validationVocabulary ? (node.minContains ?? 1) : 1; + final maximum = node.validationVocabulary ? node.maxContains : null; + if (matches < minimum) { + return fail( + node.validationVocabulary && node.minContains != null + ? 'minContains' + : 'contains', + ); + } + if (maximum != null && matches > maximum) return fail('maxContains'); + } + if (node.unevaluatedItems case final target?) { for (var i = 0; i < value.length; i++) { - final error = _checkImportedNode(target, value[i]); - if (error != null) return error.at('$i', const ['items']); + if (evaluated.items.contains(i)) continue; + final result = _evaluateImportedNode(target, value[i], currentScope); + if (result.error case final error?) { + return _ImportEvaluation( + error.at('$i', const ['unevaluatedItems']), + ); + } + evaluated.items.add(i); } } } - return null; + return evaluated; } /// The failing size keyword, if any. @@ -388,58 +698,3 @@ bool _isImportType(Object? type, Object? value) => switch (type) { 'object' => value is Map, _ => false, }; -final _importedBigIntInt64Min = BigInt.parse('-9223372036854775808'); -final _importedBigIntInt64Max = BigInt.parse('9223372036854775807'); -final _importedBigIntUint64Max = BigInt.parse('18446744073709551615'); - -bool _isValidImportedNumericFormat(String format, num value) { - switch (format) { - case 'int32': - if (!value.isFinite || value % 1 != 0) return false; - return value >= -2147483648 && value <= 2147483647; - case 'uint32': - if (!value.isFinite || value % 1 != 0) return false; - return value >= 0 && value <= 4294967295; - case 'int64': - final intVal = _integralBigInt(value); - if (intVal == null) return false; - return intVal >= _importedBigIntInt64Min && - intVal <= _importedBigIntInt64Max; - case 'uint64': - final intVal = _integralBigInt(value); - if (intVal == null) return false; - return intVal >= BigInt.zero && intVal <= _importedBigIntUint64Max; - case 'float': - if (!value.isFinite) return false; - final d = value.toDouble(); - return d.abs() <= 3.4028234663852886e+38; - case 'double': - return value.isFinite; - default: - // String formats do not constrain numeric instances. - return true; - } -} - -BigInt? _integralBigInt(num value) { - if (!value.isFinite || value % 1 != 0) return null; - if (value is int) return BigInt.from(value); - final (:coefficient, :scale) = decimalParts(value); - if (scale == 0) return coefficient; - final divisor = BigInt.from(10).pow(scale); - if (coefficient % divisor != BigInt.zero) return null; - return coefficient ~/ divisor; -} - -bool _isValidImportedStringFormat(String format, String value) => - switch (format) { - 'date-time' => isValidRfc3339DateTime(value), - 'date' => isValidIso8601Date(value), - 'email' => isValidEmailFormat(value), - 'uuid' => isValidUuidFormat(value), - 'uri' => isValidUriFormat(value), - 'ipv4' => StringIpConstraint.isIpv4(value), - 'ipv6' => StringIpConstraint.isIpv6(value), - // Numeric formats do not constrain string instances. - _ => true, - }; diff --git a/packages/ack/lib/src/schemas/schema.dart b/packages/ack/lib/src/schemas/schema.dart index 8f36d61a..d4318183 100644 --- a/packages/ack/lib/src/schemas/schema.dart +++ b/packages/ack/lib/src/schemas/schema.dart @@ -1,14 +1,16 @@ import 'dart:collection'; +import 'dart:convert'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; +import 'package:punycoder/punycoder.dart' as punycoder; +import 'package:unorm_dart/unorm_dart.dart' as unorm; import '../common_types.dart'; import '../constraints/comparison_constraint.dart'; import '../constraints/constraint.dart'; import '../constraints/number_finite_constraint.dart'; import '../constraints/pattern_constraint.dart'; -import '../constraints/string_ip_constraint.dart'; import '../constraints/validators.dart'; import '../context.dart'; import '../helpers.dart'; @@ -17,7 +19,9 @@ import '../validation/schema_error.dart'; import '../validation/schema_result.dart'; part '../json_schema/json_schema_compiler.dart'; +part '../json_schema/json_schema_formats.dart'; part '../json_schema/json_schema_import_error.dart'; +part '../json_schema/standard_meta_schemas.dart'; part 'any_of_schema.dart'; part 'any_schema.dart'; part 'boundary_schema.dart'; diff --git a/packages/ack/pubspec.yaml b/packages/ack/pubspec.yaml index f872df39..6941231c 100644 --- a/packages/ack/pubspec.yaml +++ b/packages/ack/pubspec.yaml @@ -18,6 +18,8 @@ environment: dependencies: meta: ^1.15.0 collection: ^1.18.0 + punycoder: ^0.3.0 + unorm_dart: ^0.3.2 dev_dependencies: test: ^1.25.15 diff --git a/packages/ack/test/fixtures/json_schema_import/README.md b/packages/ack/test/fixtures/json_schema_import/README.md index 0fc685b6..d2f293f8 100644 --- a/packages/ack/test/fixtures/json_schema_import/README.md +++ b/packages/ack/test/fixtures/json_schema_import/README.md @@ -3,9 +3,8 @@ Source: [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) at commit `80c87e8fca8b207a7a7ae944b875f0fcf889f46a` (MIT; see LICENSE). -`draft2020-12.json` contains the 211 test groups (690 instance cases) that use -the importer's supported subset, selected from `tests/draft2020-12/*.json` and -`tests/draft2020-12/optional/float-overflow.json`. +`draft2020-12.json` contains the 341 test groups (1,178 instance cases) that use +the importer's supported subset, selected from `tests/draft2020-12/*.json`. Descriptions are prefixed with the original filename. Schema and instance values are unchanged. Referenced fixtures from `remotes/` are included in each group's `documents` map, keyed by the upstream test-server URI, for offline use. @@ -14,5 +13,20 @@ Selection happened when importing the fixtures, not at test runtime. Every listed group must continue importing exactly; unsupported features cannot silently turn these regression tests into skips. +The `multipleOf`, `contains`, `minContains`, `maxContains`, +`dependentRequired`, `dependentSchemas`, `patternProperties`, `prefixItems`, +`if-then-else`, `unevaluatedProperties`, `unevaluatedItems`, `format` annotation, +`content` annotation, and remaining +`items` groups, plus the +Unicode-property-escape group in `pattern.json`, were added as those semantics +were implemented. Pattern compilation uses Unicode mode, as required by the +draft 2020-12 dialect. + +Two official `unevaluated*` groups use `$dynamicRef` and remain outside this +selected fixture. They, along with all 21 `dynamicRef.json` groups, run in the +complete conformance suite and the dedicated dynamic-reference test in both +source-validation and export/re-import modes. This selected fixture is not the +complete conformance suite. + The complete suite also contains unsupported keywords; this fixture selection is **not** a claim of complete draft 2020-12 conformance. diff --git a/packages/ack/test/fixtures/json_schema_import/draft2020-12.json b/packages/ack/test/fixtures/json_schema_import/draft2020-12.json index 66295a47..9cffd744 100644 --- a/packages/ack/test/fixtures/json_schema_import/draft2020-12.json +++ b/packages/ack/test/fixtures/json_schema_import/draft2020-12.json @@ -1544,6 +1544,54 @@ } ] }, + { + "description": "contains.json: items + contains", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { + "multipleOf": 2 + }, + "contains": { + "multipleOf": 3 + } + }, + "tests": [ + { + "description": "matches items, does not match contains", + "data": [ + 2, + 4, + 8 + ], + "valid": false + }, + { + "description": "does not match items, matches contains", + "data": [ + 3, + 6, + 9 + ], + "valid": false + }, + { + "description": "matches both items and contains", + "data": [ + 6, + 12 + ], + "valid": true + }, + { + "description": "matches neither items nor contains", + "data": [ + 1, + 5 + ], + "valid": false + } + ] + }, { "description": "contains.json: contains with false if subschema", "schema": { @@ -1586,6 +1634,145 @@ } ] }, + { + "description": "content.json: validation of string-encoded content based on media type", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contentMediaType": "application/json" + }, + "tests": [ + { + "description": "a valid JSON document", + "data": "{\"foo\": \"bar\"}", + "valid": true + }, + { + "description": "an invalid JSON document; validates true", + "data": "{:}", + "valid": true + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + } + ] + }, + { + "description": "content.json: validation of binary string-encoding", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contentEncoding": "base64" + }, + "tests": [ + { + "description": "a valid base64 string", + "data": "eyJmb28iOiAiYmFyIn0K", + "valid": true + }, + { + "description": "an invalid base64 string (% is not a valid character); validates true", + "data": "eyJmb28iOi%iYmFyIn0K", + "valid": true + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + } + ] + }, + { + "description": "content.json: validation of binary-encoded media type documents", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contentMediaType": "application/json", + "contentEncoding": "base64" + }, + "tests": [ + { + "description": "a valid base64-encoded JSON document", + "data": "eyJmb28iOiAiYmFyIn0K", + "valid": true + }, + { + "description": "a validly-encoded invalid JSON document; validates true", + "data": "ezp9Cg==", + "valid": true + }, + { + "description": "an invalid base64 string that is valid JSON; validates true", + "data": "{}", + "valid": true + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + } + ] + }, + { + "description": "content.json: validation of binary-encoded media type documents with schema", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contentMediaType": "application/json", + "contentEncoding": "base64", + "contentSchema": { + "type": "object", + "required": [ + "foo" + ], + "properties": { + "foo": { + "type": "string" + } + } + } + }, + "tests": [ + { + "description": "a valid base64-encoded JSON document", + "data": "eyJmb28iOiAiYmFyIn0K", + "valid": true + }, + { + "description": "another valid base64-encoded JSON document", + "data": "eyJib28iOiAyMCwgImZvbyI6ICJiYXoifQ==", + "valid": true + }, + { + "description": "an invalid base64-encoded JSON document; validates true", + "data": "eyJib28iOiAyMH0=", + "valid": true + }, + { + "description": "an empty object as a base64-encoded JSON document; validates true", + "data": "e30=", + "valid": true + }, + { + "description": "an empty array as a base64-encoded JSON document", + "data": "W10=", + "valid": true + }, + { + "description": "a validly-encoded invalid JSON document; validates true", + "data": "ezp9Cg==", + "valid": true + }, + { + "description": "an invalid base64 string that is valid JSON; validates true", + "data": "{}", + "valid": true + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + } + ] + }, { "description": "default.json: invalid type for default", "schema": { @@ -1675,5259 +1862,10877 @@ ] }, { - "description": "enum.json: simple enum validation", + "description": "dependentRequired.json: single dependency", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [ - 1, - 2, - 3 - ] - }, - "tests": [ - { - "description": "one of the enum is valid", - "data": 1, - "valid": true - }, - { - "description": "something else is invalid", - "data": 4, - "valid": false + "dependentRequired": { + "bar": [ + "foo" + ] } - ] - }, - { - "description": "enum.json: heterogeneous enum validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [ - 6, - "foo", - [], - true, - { - "foo": 12 - } - ] }, "tests": [ { - "description": "one of the enum is valid", - "data": [], + "description": "neither", + "data": {}, "valid": true }, { - "description": "something else is invalid", - "data": null, - "valid": false - }, - { - "description": "objects are deep compared", + "description": "nondependant", "data": { - "foo": false + "foo": 1 }, - "valid": false + "valid": true }, { - "description": "valid object matches", + "description": "with dependency", "data": { - "foo": 12 + "foo": 1, + "bar": 2 }, "valid": true }, { - "description": "extra properties in object is invalid", + "description": "missing dependency", "data": { - "foo": 12, - "boo": 42 + "bar": 2 }, "valid": false + }, + { + "description": "ignores arrays", + "data": [ + "bar" + ], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobar", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true } ] }, { - "description": "enum.json: heterogeneous enum-with-null validation", + "description": "dependentRequired.json: empty dependents", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [ - 6, - null - ] + "dependentRequired": { + "bar": [] + } }, "tests": [ { - "description": "null is valid", - "data": null, + "description": "empty object", + "data": {}, "valid": true }, { - "description": "number is valid", - "data": 6, + "description": "object with one property", + "data": { + "bar": 2 + }, "valid": true }, { - "description": "something else is invalid", - "data": "test", - "valid": false + "description": "non-object is valid", + "data": 1, + "valid": true } ] }, { - "description": "enum.json: enums in properties", + "description": "dependentRequired.json: multiple dependents required", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { - "enum": [ - "foo" - ] - }, - "bar": { - "enum": [ - "bar" - ] - } - }, - "required": [ - "bar" - ] + "dependentRequired": { + "quux": [ + "foo", + "bar" + ] + } }, "tests": [ { - "description": "both properties are valid", + "description": "neither", + "data": {}, + "valid": true + }, + { + "description": "nondependants", "data": { - "foo": "foo", - "bar": "bar" + "foo": 1, + "bar": 2 }, "valid": true }, { - "description": "wrong foo value", + "description": "with dependencies", "data": { - "foo": "foot", - "bar": "bar" + "foo": 1, + "bar": 2, + "quux": 3 }, - "valid": false + "valid": true }, { - "description": "wrong bar value", + "description": "missing dependency", "data": { - "foo": "foo", - "bar": "bart" + "foo": 1, + "quux": 2 }, "valid": false }, { - "description": "missing optional property is valid", + "description": "missing other dependency", "data": { - "bar": "bar" + "bar": 1, + "quux": 2 }, - "valid": true + "valid": false }, { - "description": "missing required property is invalid", + "description": "missing both dependencies", "data": { - "foo": "foo" + "quux": 1 }, "valid": false - }, - { - "description": "missing all properties is invalid", - "data": {}, - "valid": false } ] }, { - "description": "enum.json: enum with escaped characters", + "description": "dependentRequired.json: dependencies with escaped characters", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [ - "foo\nbar", - "foo\rbar" - ] + "dependentRequired": { + "foo\nbar": [ + "foo\rbar" + ], + "foo\"bar": [ + "foo'bar" + ] + } }, "tests": [ { - "description": "member 1 is valid", - "data": "foo\nbar", + "description": "CRLF", + "data": { + "foo\nbar": 1, + "foo\rbar": 2 + }, "valid": true }, { - "description": "member 2 is valid", - "data": "foo\rbar", + "description": "quoted quotes", + "data": { + "foo'bar": 1, + "foo\"bar": 2 + }, "valid": true }, { - "description": "another string is invalid", - "data": "abc", + "description": "CRLF missing dependent", + "data": { + "foo\nbar": 1, + "foo": 2 + }, + "valid": false + }, + { + "description": "quoted quotes missing dependent", + "data": { + "foo\"bar": 2 + }, "valid": false } ] }, { - "description": "enum.json: enum with false does not match 0", + "description": "dependentSchemas.json: single dependency", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [ - false - ] + "dependentSchemas": { + "bar": { + "properties": { + "foo": { + "type": "integer" + }, + "bar": { + "type": "integer" + } + } + } + } }, "tests": [ { - "description": "false is valid", - "data": false, + "description": "valid", + "data": { + "foo": 1, + "bar": 2 + }, "valid": true }, { - "description": "integer zero is invalid", - "data": 0, + "description": "no dependency", + "data": { + "foo": "quux" + }, + "valid": true + }, + { + "description": "wrong type", + "data": { + "foo": "quux", + "bar": 2 + }, "valid": false }, { - "description": "float zero is invalid", - "data": 0.0, + "description": "wrong type other", + "data": { + "foo": 2, + "bar": "quux" + }, "valid": false - } - ] - }, - { - "description": "enum.json: enum with [false] does not match [0]", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [ - [ - false - ] - ] - }, - "tests": [ + }, { - "description": "[false] is valid", + "description": "wrong type both", + "data": { + "foo": "quux", + "bar": "quux" + }, + "valid": false + }, + { + "description": "ignores arrays", "data": [ - false + "bar" ], "valid": true }, { - "description": "[0] is invalid", - "data": [ - 0 - ], - "valid": false + "description": "ignores strings", + "data": "foobar", + "valid": true }, { - "description": "[0.0] is invalid", - "data": [ - 0.0 - ], - "valid": false + "description": "ignores other non-objects", + "data": 12, + "valid": true } ] }, { - "description": "enum.json: enum with true does not match 1", + "description": "dependentSchemas.json: boolean subschemas", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [ - true - ] + "dependentSchemas": { + "foo": true, + "bar": false + } }, "tests": [ { - "description": "true is valid", - "data": true, + "description": "object with property having schema true is valid", + "data": { + "foo": 1 + }, "valid": true }, { - "description": "integer one is invalid", - "data": 1, + "description": "object with property having schema false is invalid", + "data": { + "bar": 2 + }, "valid": false }, { - "description": "float one is invalid", - "data": 1.0, + "description": "object with both properties is invalid", + "data": { + "foo": 1, + "bar": 2 + }, "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true } ] }, { - "description": "enum.json: enum with [true] does not match [1]", + "description": "dependentSchemas.json: dependencies with escaped characters", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [ - [ - true - ] - ] + "dependentSchemas": { + "foo\tbar": { + "minProperties": 4 + }, + "foo'bar": { + "required": [ + "foo\"bar" + ] + } + } }, "tests": [ { - "description": "[true] is valid", - "data": [ - true - ], + "description": "quoted tab", + "data": { + "foo\tbar": 1, + "a": 2, + "b": 3, + "c": 4 + }, "valid": true }, { - "description": "[1] is invalid", - "data": [ - 1 - ], + "description": "quoted quote", + "data": { + "foo'bar": { + "foo\"bar": 1 + } + }, "valid": false }, { - "description": "[1.0] is invalid", - "data": [ - 1.0 - ], + "description": "quoted tab invalid under dependent schema", + "data": { + "foo\tbar": 1, + "a": 2 + }, + "valid": false + }, + { + "description": "quoted quote invalid under dependent schema", + "data": { + "foo'bar": 1 + }, "valid": false } ] }, { - "description": "enum.json: enum with 0 does not match false", + "description": "dependentSchemas.json: dependent subschema incompatible with root", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [ - 0 - ] + "properties": { + "foo": {} + }, + "dependentSchemas": { + "foo": { + "properties": { + "bar": {} + }, + "additionalProperties": false + } + } }, "tests": [ { - "description": "false is invalid", - "data": false, + "description": "matches root", + "data": { + "foo": 1 + }, "valid": false }, { - "description": "integer zero is valid", - "data": 0, + "description": "matches dependency", + "data": { + "bar": 1 + }, "valid": true }, { - "description": "float zero is valid", - "data": 0.0, + "description": "matches both", + "data": { + "foo": 1, + "bar": 2 + }, + "valid": false + }, + { + "description": "no dependency", + "data": { + "baz": 1 + }, "valid": true } ] }, { - "description": "enum.json: enum with [0] does not match [false]", + "description": "enum.json: simple enum validation", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "enum": [ - [ - 0 - ] + 1, + 2, + 3 ] }, "tests": [ { - "description": "[false] is invalid", - "data": [ - false - ], - "valid": false - }, - { - "description": "[0] is valid", - "data": [ - 0 - ], + "description": "one of the enum is valid", + "data": 1, "valid": true }, { - "description": "[0.0] is valid", - "data": [ - 0.0 - ], - "valid": true + "description": "something else is invalid", + "data": 4, + "valid": false } ] }, { - "description": "enum.json: enum with 1 does not match true", + "description": "enum.json: heterogeneous enum validation", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "enum": [ - 1 + 6, + "foo", + [], + true, + { + "foo": 12 + } ] }, "tests": [ { - "description": "true is invalid", - "data": true, - "valid": false - }, - { - "description": "integer one is valid", - "data": 1, + "description": "one of the enum is valid", + "data": [], "valid": true }, { - "description": "float one is valid", - "data": 1.0, - "valid": true - } - ] - }, - { - "description": "enum.json: enum with [1] does not match [true]", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [ - [ - 1 - ] - ] - }, - "tests": [ + "description": "something else is invalid", + "data": null, + "valid": false + }, { - "description": "[true] is invalid", - "data": [ - true - ], + "description": "objects are deep compared", + "data": { + "foo": false + }, "valid": false }, { - "description": "[1] is valid", - "data": [ - 1 - ], + "description": "valid object matches", + "data": { + "foo": 12 + }, "valid": true }, { - "description": "[1.0] is valid", - "data": [ - 1.0 - ], - "valid": true + "description": "extra properties in object is invalid", + "data": { + "foo": 12, + "boo": 42 + }, + "valid": false } ] }, { - "description": "enum.json: nul characters in strings", + "description": "enum.json: heterogeneous enum-with-null validation", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "enum": [ - "hello\u0000there" + 6, + null ] }, "tests": [ { - "description": "match string with nul", - "data": "hello\u0000there", + "description": "null is valid", + "data": null, "valid": true }, { - "description": "do not match string lacking nul", - "data": "hellothere", + "description": "number is valid", + "data": 6, + "valid": true + }, + { + "description": "something else is invalid", + "data": "test", "valid": false } ] }, { - "description": "enum.json: empty enum", + "description": "enum.json: enums in properties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "enum": [] + "type": "object", + "properties": { + "foo": { + "enum": [ + "foo" + ] + }, + "bar": { + "enum": [ + "bar" + ] + } + }, + "required": [ + "bar" + ] }, "tests": [ { - "description": "string is invalid", - "data": "foo", - "valid": false + "description": "both properties are valid", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true }, { - "description": "number is invalid", - "data": 42, + "description": "wrong foo value", + "data": { + "foo": "foot", + "bar": "bar" + }, "valid": false }, { - "description": "null is invalid", - "data": null, + "description": "wrong bar value", + "data": { + "foo": "foo", + "bar": "bart" + }, "valid": false }, { - "description": "object is invalid", - "data": {}, - "valid": false + "description": "missing optional property is valid", + "data": { + "bar": "bar" + }, + "valid": true }, { - "description": "array is invalid", - "data": [], + "description": "missing required property is invalid", + "data": { + "foo": "foo" + }, "valid": false }, { - "description": "boolean is invalid", - "data": false, + "description": "missing all properties is invalid", + "data": {}, "valid": false } ] }, { - "description": "enum.json: characters with the same visual representation, but different number of codepoints", + "description": "enum.json: enum with escaped characters", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "enum": [ - "ä" - ], - "$comment": "U+00E4" + "foo\nbar", + "foo\rbar" + ] }, "tests": [ { - "description": "character uses the same codepoint", - "data": "ä", - "comment": "U+00E4", + "description": "member 1 is valid", + "data": "foo\nbar", "valid": true }, { - "description": "character looks the same but uses combining marks", - "data": "ä", - "comment": "a, U+0308", + "description": "member 2 is valid", + "data": "foo\rbar", + "valid": true + }, + { + "description": "another string is invalid", + "data": "abc", "valid": false } ] }, { - "description": "exclusiveMaximum.json: exclusiveMaximum validation", + "description": "enum.json: enum with false does not match 0", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "exclusiveMaximum": 3.0 + "enum": [ + false + ] }, "tests": [ { - "description": "below the exclusiveMaximum is valid", - "data": 2.2, + "description": "false is valid", + "data": false, "valid": true }, { - "description": "boundary point is invalid", - "data": 3.0, + "description": "integer zero is invalid", + "data": 0, "valid": false }, { - "description": "above the exclusiveMaximum is invalid", - "data": 3.5, + "description": "float zero is invalid", + "data": 0.0, "valid": false - }, - { - "description": "ignores non-numbers", - "data": "x", - "valid": true } ] }, { - "description": "exclusiveMinimum.json: exclusiveMinimum validation", + "description": "enum.json: enum with [false] does not match [0]", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "exclusiveMinimum": 1.1 + "enum": [ + [ + false + ] + ] }, "tests": [ { - "description": "above the exclusiveMinimum is valid", - "data": 1.2, + "description": "[false] is valid", + "data": [ + false + ], "valid": true }, { - "description": "boundary point is invalid", - "data": 1.1, + "description": "[0] is invalid", + "data": [ + 0 + ], "valid": false }, { - "description": "below the exclusiveMinimum is invalid", - "data": 0.6, + "description": "[0.0] is invalid", + "data": [ + 0.0 + ], "valid": false - }, - { - "description": "ignores non-numbers", - "data": "x", - "valid": true } ] }, { - "description": "if-then-else.json: ignore if without then or else", + "description": "enum.json: enum with true does not match 1", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": { - "const": 0 - } + "enum": [ + true + ] }, "tests": [ { - "description": "valid when valid against lone if", - "data": 0, + "description": "true is valid", + "data": true, "valid": true }, { - "description": "valid when invalid against lone if", - "data": "hello", - "valid": true + "description": "integer one is invalid", + "data": 1, + "valid": false + }, + { + "description": "float one is invalid", + "data": 1.0, + "valid": false } ] }, { - "description": "if-then-else.json: ignore then without if", + "description": "enum.json: enum with [true] does not match [1]", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "then": { - "const": 0 - } - }, + "enum": [ + [ + true + ] + ] + }, "tests": [ { - "description": "valid when valid against lone then", - "data": 0, + "description": "[true] is valid", + "data": [ + true + ], "valid": true }, { - "description": "valid when invalid against lone then", - "data": "hello", - "valid": true + "description": "[1] is invalid", + "data": [ + 1 + ], + "valid": false + }, + { + "description": "[1.0] is invalid", + "data": [ + 1.0 + ], + "valid": false } ] }, { - "description": "if-then-else.json: ignore else without if", + "description": "enum.json: enum with 0 does not match false", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "else": { - "const": 0 - } + "enum": [ + 0 + ] }, "tests": [ { - "description": "valid when valid against lone else", + "description": "false is invalid", + "data": false, + "valid": false + }, + { + "description": "integer zero is valid", "data": 0, "valid": true }, { - "description": "valid when invalid against lone else", - "data": "hello", + "description": "float zero is valid", + "data": 0.0, "valid": true } ] }, { - "description": "if-then-else.json: if and then without else", + "description": "enum.json: enum with [0] does not match [false]", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": { - "exclusiveMaximum": 0 - }, - "then": { - "minimum": -10 - } + "enum": [ + [ + 0 + ] + ] }, "tests": [ { - "description": "valid through then", - "data": -1, - "valid": true + "description": "[false] is invalid", + "data": [ + false + ], + "valid": false }, { - "description": "invalid through then", - "data": -100, - "valid": false + "description": "[0] is valid", + "data": [ + 0 + ], + "valid": true }, { - "description": "valid when if test fails", - "data": 3, + "description": "[0.0] is valid", + "data": [ + 0.0 + ], "valid": true } ] }, { - "description": "if-then-else.json: if with boolean schema true", + "description": "enum.json: enum with 1 does not match true", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": true, - "then": { - "const": "then" - }, - "else": { - "const": "else" - } + "enum": [ + 1 + ] }, "tests": [ { - "description": "boolean schema true in if always chooses the then path (valid)", - "data": "then", + "description": "true is invalid", + "data": true, + "valid": false + }, + { + "description": "integer one is valid", + "data": 1, "valid": true }, { - "description": "boolean schema true in if always chooses the then path (invalid)", - "data": "else", - "valid": false + "description": "float one is valid", + "data": 1.0, + "valid": true } ] }, { - "description": "if-then-else.json: if with boolean schema false", + "description": "enum.json: enum with [1] does not match [true]", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "if": false, - "then": { - "const": "then" - }, - "else": { - "const": "else" - } + "enum": [ + [ + 1 + ] + ] }, "tests": [ { - "description": "boolean schema false in if always chooses the else path (invalid)", - "data": "then", + "description": "[true] is invalid", + "data": [ + true + ], "valid": false }, { - "description": "boolean schema false in if always chooses the else path (valid)", - "data": "else", + "description": "[1] is valid", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "[1.0] is valid", + "data": [ + 1.0 + ], "valid": true } ] }, { - "description": "if-then-else.json: if appears at the end when serialized (keyword processing sequence)", + "description": "enum.json: nul characters in strings", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "then": { - "const": "yes" - }, - "else": { - "const": "other" + "enum": [ + "hello\u0000there" + ] + }, + "tests": [ + { + "description": "match string with nul", + "data": "hello\u0000there", + "valid": true }, - "if": { - "maxLength": 4 + { + "description": "do not match string lacking nul", + "data": "hellothere", + "valid": false } + ] + }, + { + "description": "enum.json: empty enum", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [] }, "tests": [ { - "description": "yes redirects to then and passes", - "data": "yes", - "valid": true + "description": "string is invalid", + "data": "foo", + "valid": false }, { - "description": "other redirects to else and passes", - "data": "other", - "valid": true + "description": "number is invalid", + "data": 42, + "valid": false }, { - "description": "no redirects to then and fails", - "data": "no", + "description": "null is invalid", + "data": null, "valid": false }, { - "description": "invalid redirects to else and fails", - "data": "invalid", + "description": "object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": [], + "valid": false + }, + { + "description": "boolean is invalid", + "data": false, "valid": false } ] }, { - "description": "if-then-else.json: then: false fails when condition matches", + "description": "enum.json: characters with the same visual representation, but different number of codepoints", "schema": { - "if": { - "const": 1 - }, - "then": false + "$schema": "https://json-schema.org/draft/2020-12/schema", + "enum": [ + "ä" + ], + "$comment": "U+00E4" }, "tests": [ { - "description": "matches if → then=false → invalid", - "data": 1, - "valid": false + "description": "character uses the same codepoint", + "data": "ä", + "comment": "U+00E4", + "valid": true }, { - "description": "does not match if → then ignored → valid", - "data": 2, - "valid": true + "description": "character looks the same but uses combining marks", + "data": "ä", + "comment": "a, U+0308", + "valid": false } ] }, { - "description": "if-then-else.json: else: false fails when condition does not match", + "description": "exclusiveMaximum.json: exclusiveMaximum validation", "schema": { - "if": { - "const": 1 - }, - "else": false + "$schema": "https://json-schema.org/draft/2020-12/schema", + "exclusiveMaximum": 3.0 }, "tests": [ { - "description": "matches if → else ignored → valid", - "data": 1, + "description": "below the exclusiveMaximum is valid", + "data": 2.2, "valid": true }, { - "description": "does not match if → else executes → invalid", - "data": 2, + "description": "boundary point is invalid", + "data": 3.0, + "valid": false + }, + { + "description": "above the exclusiveMaximum is invalid", + "data": 3.5, "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true } ] }, { - "description": "infinite-loop-detection.json: evaluating the same schema location against the same data location twice is not a sign of an infinite loop", + "description": "exclusiveMinimum.json: exclusiveMinimum validation", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "int": { - "type": "integer" - } - }, - "allOf": [ - { - "properties": { - "foo": { - "$ref": "#/$defs/int" - } - } - }, - { - "additionalProperties": { - "$ref": "#/$defs/int" - } - } - ] + "exclusiveMinimum": 1.1 }, "tests": [ { - "description": "passing case", - "data": { - "foo": 1 - }, + "description": "above the exclusiveMinimum is valid", + "data": 1.2, "valid": true }, { - "description": "failing case", - "data": { - "foo": "a string" - }, + "description": "boundary point is invalid", + "data": 1.1, + "valid": false + }, + { + "description": "below the exclusiveMinimum is invalid", + "data": 0.6, "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true } ] }, { - "description": "items.json: a schema given for items", + "description": "format.json: email format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "items": { - "type": "integer" - } + "format": "email" }, "tests": [ { - "description": "valid items", - "data": [ - 1, - 2, - 3 - ], + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "wrong type of items", - "data": [ - 1, - "x" - ], - "valid": false + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true }, { - "description": "ignores non-arrays", - "data": { - "foo": "bar" - }, + "description": "all string formats ignore objects", + "data": {}, "valid": true }, { - "description": "JavaScript pseudo-array is valid", - "data": { - "0": "invalid", - "length": 1 - }, + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid email string is only an annotation by default", + "data": "2962", "valid": true } ] }, { - "description": "items.json: items with boolean schema (true)", + "description": "format.json: idn-email format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "items": true + "format": "idn-email" }, "tests": [ { - "description": "any array is valid", - "data": [ - 1, - "foo", - true - ], + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "empty array is valid", + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", "data": [], "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid idn-email string is only an annotation by default", + "data": "2962", + "valid": true } ] }, { - "description": "items.json: items with boolean schema (false)", + "description": "format.json: regex format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "items": false + "format": "regex" }, "tests": [ { - "description": "any non-empty array is invalid", - "data": [ - 1, - "foo", - true - ], - "valid": false + "description": "all string formats ignore integers", + "data": 12, + "valid": true }, { - "description": "empty array is valid", + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", "data": [], "valid": true - } - ] - }, - { - "description": "items.json: nested items", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "array", - "items": { - "type": "number" - } - } - } - } - }, - "tests": [ + }, { - "description": "valid nested array", - "data": [ - [ - [ - [ - 1 - ] - ], - [ - [ - 2 - ], - [ - 3 - ] - ] - ], - [ - [ - [ - 4 - ], - [ - 5 - ], - [ - 6 - ] - ] - ] - ], + "description": "all string formats ignore booleans", + "data": false, "valid": true }, { - "description": "nested array with invalid type", - "data": [ - [ - [ - [ - "1" - ] - ], - [ - [ - 2 - ], - [ - 3 - ] - ] - ], - [ - [ - [ - 4 - ], - [ - 5 - ], - [ - 6 - ] - ] - ] - ], - "valid": false + "description": "all string formats ignore nulls", + "data": null, + "valid": true }, { - "description": "not deep enough", - "data": [ - [ - [ - 1 - ], - [ - 2 - ], - [ - 3 - ] - ], - [ - [ - 4 - ], - [ - 5 - ], - [ - 6 - ] - ] - ], - "valid": false + "description": "invalid regex string is only an annotation by default", + "data": "^(abc]", + "valid": true } ] }, { - "description": "items.json: items with null instance elements", + "description": "format.json: ipv4 format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "items": { - "type": "null" - } + "format": "ipv4" }, "tests": [ { - "description": "allows null elements", - "data": [ - null - ], + "description": "all string formats ignore integers", + "data": 12, "valid": true - } - ] - }, - { - "description": "maxItems.json: maxItems validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "maxItems": 2 - }, - "tests": [ + }, { - "description": "shorter is valid", - "data": [ - 1 - ], + "description": "all string formats ignore floats", + "data": 13.7, "valid": true }, { - "description": "exact length is valid", - "data": [ - 1, - 2 - ], + "description": "all string formats ignore objects", + "data": {}, "valid": true }, { - "description": "too long is invalid", - "data": [ - 1, - 2, - 3 - ], - "valid": false + "description": "all string formats ignore arrays", + "data": [], + "valid": true }, { - "description": "ignores non-arrays", - "data": "foobar", + "description": "all string formats ignore booleans", + "data": false, "valid": true - } - ] - }, - { - "description": "maxItems.json: maxItems validation with a decimal", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "maxItems": 2.0 - }, - "tests": [ + }, { - "description": "shorter is valid", - "data": [ - 1 - ], + "description": "all string formats ignore nulls", + "data": null, "valid": true }, { - "description": "too long is invalid", - "data": [ - 1, - 2, - 3 - ], - "valid": false + "description": "invalid ipv4 string is only an annotation by default", + "data": "127.0.0.0.1", + "valid": true } ] }, { - "description": "maxLength.json: maxLength validation", + "description": "format.json: ipv6 format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "maxLength": 2 + "format": "ipv6" }, "tests": [ { - "description": "shorter is valid", - "data": "f", + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "exact length is valid", - "data": "fo", + "description": "all string formats ignore floats", + "data": 13.7, "valid": true }, { - "description": "too long is invalid", - "data": "foo", - "valid": false + "description": "all string formats ignore objects", + "data": {}, + "valid": true }, { - "description": "ignores non-strings", - "data": 100, + "description": "all string formats ignore arrays", + "data": [], "valid": true }, { - "description": "two graphemes is long enough", - "data": "💩💩", + "description": "all string formats ignore booleans", + "data": false, "valid": true - } - ] - }, - { - "description": "maxLength.json: maxLength validation with a decimal", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "maxLength": 2.0 - }, - "tests": [ + }, { - "description": "shorter is valid", - "data": "f", + "description": "all string formats ignore nulls", + "data": null, "valid": true }, { - "description": "too long is invalid", - "data": "foo", - "valid": false + "description": "invalid ipv6 string is only an annotation by default", + "data": "12345::", + "valid": true } ] }, { - "description": "maxProperties.json: maxProperties validation", + "description": "format.json: idn-hostname format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "maxProperties": 2 + "format": "idn-hostname" }, "tests": [ { - "description": "shorter is valid", - "data": { - "foo": 1 - }, + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "exact length is valid", - "data": { - "foo": 1, - "bar": 2 - }, + "description": "all string formats ignore floats", + "data": 13.7, "valid": true }, { - "description": "too long is invalid", - "data": { - "foo": 1, - "bar": 2, - "baz": 3 - }, - "valid": false + "description": "all string formats ignore objects", + "data": {}, + "valid": true }, { - "description": "ignores arrays", - "data": [ - 1, - 2, - 3 - ], + "description": "all string formats ignore arrays", + "data": [], "valid": true }, { - "description": "ignores strings", - "data": "foobar", + "description": "all string formats ignore booleans", + "data": false, "valid": true }, { - "description": "ignores other non-objects", - "data": 12, + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid idn-hostname string is only an annotation by default", + "data": "〮실례.테스트", "valid": true } ] }, { - "description": "maxProperties.json: maxProperties validation with a decimal", + "description": "format.json: hostname format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "maxProperties": 2.0 + "format": "hostname" }, "tests": [ { - "description": "shorter is valid", - "data": { - "foo": 1 - }, + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "too long is invalid", - "data": { - "foo": 1, - "bar": 2, - "baz": 3 - }, - "valid": false - } - ] - }, - { - "description": "maxProperties.json: maxProperties = 0 means the object is empty", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "maxProperties": 0 - }, - "tests": [ + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, { - "description": "no properties is valid", + "description": "all string formats ignore objects", "data": {}, "valid": true }, { - "description": "one property is invalid", - "data": { - "foo": 1 - }, - "valid": false + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid hostname string is only an annotation by default", + "data": "-a-host-name-that-starts-with--", + "valid": true } ] }, { - "description": "maximum.json: maximum validation", + "description": "format.json: date format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "maximum": 3.0 + "format": "date" }, "tests": [ { - "description": "below the maximum is valid", - "data": 2.6, + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "boundary point is valid", - "data": 3.0, + "description": "all string formats ignore floats", + "data": 13.7, "valid": true }, { - "description": "above the maximum is invalid", - "data": 3.5, - "valid": false + "description": "all string formats ignore objects", + "data": {}, + "valid": true }, { - "description": "ignores non-numbers", - "data": "x", + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid date string is only an annotation by default", + "data": "06/19/1963", "valid": true } ] }, { - "description": "maximum.json: maximum validation with unsigned integer", + "description": "format.json: date-time format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "maximum": 300 + "format": "date-time" }, "tests": [ { - "description": "below the maximum is valid", - "data": 299.97, + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "boundary point integer is valid", - "data": 300, + "description": "all string formats ignore floats", + "data": 13.7, "valid": true }, { - "description": "boundary point float is valid", - "data": 300.0, + "description": "all string formats ignore objects", + "data": {}, "valid": true }, { - "description": "above the maximum is invalid", - "data": 300.5, - "valid": false + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid date-time string is only an annotation by default", + "data": "1990-02-31T15:59:60.123-08:00", + "valid": true } ] }, { - "description": "minItems.json: minItems validation", + "description": "format.json: time format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "minItems": 1 + "format": "time" }, "tests": [ { - "description": "longer is valid", - "data": [ - 1, - 2 - ], + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "exact length is valid", - "data": [ - 1 - ], + "description": "all string formats ignore floats", + "data": 13.7, "valid": true }, { - "description": "too short is invalid", + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", "data": [], - "valid": false + "valid": true }, { - "description": "ignores non-arrays", - "data": "", + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid time string is only an annotation by default", + "data": "08:30:06 PST", "valid": true } ] }, { - "description": "minItems.json: minItems validation with a decimal", + "description": "format.json: json-pointer format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "minItems": 1.0 + "format": "json-pointer" }, "tests": [ { - "description": "longer is valid", - "data": [ - 1, - 2 - ], + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "too short is invalid", + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", "data": [], - "valid": false + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid json-pointer string is only an annotation by default", + "data": "/foo/bar~", + "valid": true } ] }, { - "description": "minLength.json: minLength validation", + "description": "format.json: relative-json-pointer format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "minLength": 2 + "format": "relative-json-pointer" }, "tests": [ { - "description": "longer is valid", - "data": "foo", + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "exact length is valid", - "data": "fo", + "description": "all string formats ignore floats", + "data": 13.7, "valid": true }, { - "description": "too short is invalid", - "data": "f", - "valid": false + "description": "all string formats ignore objects", + "data": {}, + "valid": true }, { - "description": "ignores non-strings", - "data": 1, + "description": "all string formats ignore arrays", + "data": [], "valid": true }, { - "description": "one grapheme is not long enough", - "data": "💩", - "valid": false - } - ] - }, - { - "description": "minLength.json: minLength validation with a decimal", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "minLength": 2.0 - }, - "tests": [ + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, { - "description": "longer is valid", - "data": "foo", + "description": "all string formats ignore nulls", + "data": null, "valid": true }, { - "description": "too short is invalid", - "data": "f", - "valid": false + "description": "invalid relative-json-pointer string is only an annotation by default", + "data": "/foo/bar", + "valid": true } ] }, { - "description": "minProperties.json: minProperties validation", + "description": "format.json: iri format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "minProperties": 1 + "format": "iri" }, "tests": [ { - "description": "longer is valid", - "data": { - "foo": 1, - "bar": 2 - }, + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "exact length is valid", - "data": { - "foo": 1 - }, + "description": "all string formats ignore floats", + "data": 13.7, "valid": true }, { - "description": "too short is invalid", + "description": "all string formats ignore objects", "data": {}, - "valid": false - }, - { - "description": "ignores arrays", - "data": [], "valid": true }, { - "description": "ignores strings", - "data": "", + "description": "all string formats ignore arrays", + "data": [], "valid": true }, { - "description": "ignores other non-objects", - "data": 12, + "description": "all string formats ignore booleans", + "data": false, "valid": true }, { - "description": "ignores null", + "description": "all string formats ignore nulls", "data": null, "valid": true }, { - "description": "ignores booleans", - "data": true, + "description": "invalid iri string is only an annotation by default", + "data": "http://2001:0db8:85a3:0000:0000:8a2e:0370:7334", "valid": true } ] }, { - "description": "minProperties.json: minProperties validation with a decimal", + "description": "format.json: iri-reference format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "minProperties": 1.0 + "format": "iri-reference" }, "tests": [ { - "description": "longer is valid", - "data": { - "foo": 1, - "bar": 2 - }, + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "too short is invalid", + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", "data": {}, - "valid": false + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid iri-reference string is only an annotation by default", + "data": "\\\\WINDOWS\\filëßåré", + "valid": true } ] }, { - "description": "minimum.json: minimum validation", + "description": "format.json: uri format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "minimum": 1.1 + "format": "uri" }, "tests": [ { - "description": "above the minimum is valid", - "data": 2.6, + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "boundary point is valid", - "data": 1.1, + "description": "all string formats ignore floats", + "data": 13.7, "valid": true }, { - "description": "below the minimum is invalid", - "data": 0.6, - "valid": false + "description": "all string formats ignore objects", + "data": {}, + "valid": true }, { - "description": "ignores non-numbers", - "data": "x", + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid uri string is only an annotation by default", + "data": "//foo.bar/?baz=qux#quux", "valid": true } ] }, { - "description": "minimum.json: minimum validation with signed integer", + "description": "format.json: uri-reference format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "minimum": -2 + "format": "uri-reference" }, "tests": [ { - "description": "negative above the minimum is valid", - "data": -1, + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "positive above the minimum is valid", - "data": 0, + "description": "all string formats ignore floats", + "data": 13.7, "valid": true }, { - "description": "boundary point is valid", - "data": -2, + "description": "all string formats ignore objects", + "data": {}, "valid": true }, { - "description": "boundary point with float is valid", - "data": -2.0, + "description": "all string formats ignore arrays", + "data": [], "valid": true }, { - "description": "float below the minimum is invalid", - "data": -2.0001, - "valid": false + "description": "all string formats ignore booleans", + "data": false, + "valid": true }, { - "description": "int below the minimum is invalid", - "data": -3, - "valid": false + "description": "all string formats ignore nulls", + "data": null, + "valid": true }, { - "description": "ignores non-numbers", - "data": "x", + "description": "invalid uri-reference string is only an annotation by default", + "data": "\\\\WINDOWS\\fileshare", "valid": true } ] }, { - "description": "not.json: not", + "description": "format.json: uri-template format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": { - "type": "integer" - } + "format": "uri-template" }, "tests": [ { - "description": "allowed", - "data": "foo", + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "disallowed", - "data": 1, - "valid": false + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid uri-template string is only an annotation by default", + "data": "http://example.com/dictionary/{term:1}/{term", + "valid": true } ] }, { - "description": "not.json: not multiple types", + "description": "format.json: uuid format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": { - "type": [ - "integer", - "boolean" - ] - } + "format": "uuid" }, "tests": [ { - "description": "valid", - "data": "foo", + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "mismatch", - "data": 1, - "valid": false + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true }, { - "description": "other mismatch", - "data": true, - "valid": false + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid uuid string is only an annotation by default", + "data": "2eb8aa08-aa98-11ea-b4aa-73b441d1638", + "valid": true } ] }, { - "description": "not.json: not more complex schema", + "description": "format.json: duration format", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": { - "type": "object", - "properties": { - "foo": { - "type": "string" - } - } - } + "format": "duration" }, "tests": [ { - "description": "match", - "data": 1, + "description": "all string formats ignore integers", + "data": 12, "valid": true }, { - "description": "other match", - "data": { - "foo": 1 - }, + "description": "all string formats ignore floats", + "data": 13.7, "valid": true }, { - "description": "mismatch", - "data": { - "foo": "bar" - }, - "valid": false - } - ] + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "invalid duration string is only an annotation by default", + "data": "PT1D", + "valid": true + } + ] }, { - "description": "not.json: forbidden property", + "description": "if-then-else.json: ignore if without then or else", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo": { - "not": {} - } + "if": { + "const": 0 } }, "tests": [ { - "description": "property present", - "data": { - "foo": 1, - "bar": 2 - }, - "valid": false + "description": "valid when valid against lone if", + "data": 0, + "valid": true }, { - "description": "property absent", - "data": { - "bar": 1, - "baz": 2 - }, + "description": "valid when invalid against lone if", + "data": "hello", "valid": true } ] }, { - "description": "not.json: forbid everything with empty schema", + "description": "if-then-else.json: ignore then without if", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": {} + "then": { + "const": 0 + } }, "tests": [ { - "description": "number is invalid", - "data": 1, - "valid": false - }, - { - "description": "string is invalid", - "data": "foo", - "valid": false - }, - { - "description": "boolean true is invalid", - "data": true, - "valid": false + "description": "valid when valid against lone then", + "data": 0, + "valid": true }, { - "description": "boolean false is invalid", - "data": false, - "valid": false - }, + "description": "valid when invalid against lone then", + "data": "hello", + "valid": true + } + ] + }, + { + "description": "if-then-else.json: ignore else without if", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "else": { + "const": 0 + } + }, + "tests": [ { - "description": "null is invalid", - "data": null, - "valid": false + "description": "valid when valid against lone else", + "data": 0, + "valid": true }, { - "description": "object is invalid", - "data": { - "foo": "bar" - }, - "valid": false + "description": "valid when invalid against lone else", + "data": "hello", + "valid": true + } + ] + }, + { + "description": "if-then-else.json: if and then without else", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": { + "exclusiveMaximum": 0 }, + "then": { + "minimum": -10 + } + }, + "tests": [ { - "description": "empty object is invalid", - "data": {}, - "valid": false + "description": "valid through then", + "data": -1, + "valid": true }, { - "description": "array is invalid", - "data": [ - "foo" - ], + "description": "invalid through then", + "data": -100, "valid": false }, { - "description": "empty array is invalid", - "data": [], - "valid": false + "description": "valid when if test fails", + "data": 3, + "valid": true } ] }, { - "description": "not.json: forbid everything with boolean schema true", + "description": "if-then-else.json: if and else without then", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": true + "if": { + "exclusiveMaximum": 0 + }, + "else": { + "multipleOf": 2 + } }, "tests": [ { - "description": "number is invalid", - "data": 1, - "valid": false - }, - { - "description": "string is invalid", - "data": "foo", - "valid": false + "description": "valid when if test passes", + "data": -1, + "valid": true }, { - "description": "boolean true is invalid", - "data": true, - "valid": false + "description": "valid through else", + "data": 4, + "valid": true }, { - "description": "boolean false is invalid", - "data": false, + "description": "invalid through else", + "data": 3, "valid": false + } + ] + }, + { + "description": "if-then-else.json: validate against correct branch, then vs else", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": { + "exclusiveMaximum": 0 }, - { - "description": "null is invalid", - "data": null, - "valid": false + "then": { + "minimum": -10 }, + "else": { + "multipleOf": 2 + } + }, + "tests": [ { - "description": "object is invalid", - "data": { - "foo": "bar" - }, - "valid": false + "description": "valid through then", + "data": -1, + "valid": true }, { - "description": "empty object is invalid", - "data": {}, + "description": "invalid through then", + "data": -100, "valid": false }, { - "description": "array is invalid", - "data": [ - "foo" - ], - "valid": false + "description": "valid through else", + "data": 4, + "valid": true }, { - "description": "empty array is invalid", - "data": [], + "description": "invalid through else", + "data": 3, "valid": false } ] }, { - "description": "not.json: allow everything with boolean schema false", + "description": "if-then-else.json: non-interference across combined schemas", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": false + "allOf": [ + { + "if": { + "exclusiveMaximum": 0 + } + }, + { + "then": { + "minimum": -10 + } + }, + { + "else": { + "multipleOf": 2 + } + } + ] }, "tests": [ { - "description": "number is valid", - "data": 1, + "description": "valid, but would have been invalid through then", + "data": -100, "valid": true }, { - "description": "string is valid", - "data": "foo", + "description": "valid, but would have been invalid through else", + "data": 3, "valid": true + } + ] + }, + { + "description": "if-then-else.json: if with boolean schema true", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": true, + "then": { + "const": "then" }, + "else": { + "const": "else" + } + }, + "tests": [ { - "description": "boolean true is valid", - "data": true, + "description": "boolean schema true in if always chooses the then path (valid)", + "data": "then", "valid": true }, { - "description": "boolean false is valid", - "data": false, - "valid": true + "description": "boolean schema true in if always chooses the then path (invalid)", + "data": "else", + "valid": false + } + ] + }, + { + "description": "if-then-else.json: if with boolean schema false", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "if": false, + "then": { + "const": "then" }, + "else": { + "const": "else" + } + }, + "tests": [ { - "description": "null is valid", - "data": null, - "valid": true + "description": "boolean schema false in if always chooses the else path (invalid)", + "data": "then", + "valid": false }, { - "description": "object is valid", - "data": { - "foo": "bar" - }, + "description": "boolean schema false in if always chooses the else path (valid)", + "data": "else", "valid": true + } + ] + }, + { + "description": "if-then-else.json: if appears at the end when serialized (keyword processing sequence)", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "then": { + "const": "yes" + }, + "else": { + "const": "other" }, + "if": { + "maxLength": 4 + } + }, + "tests": [ { - "description": "empty object is valid", - "data": {}, + "description": "yes redirects to then and passes", + "data": "yes", "valid": true }, { - "description": "array is valid", - "data": [ - "foo" - ], + "description": "other redirects to else and passes", + "data": "other", "valid": true }, { - "description": "empty array is valid", - "data": [], - "valid": true + "description": "no redirects to then and fails", + "data": "no", + "valid": false + }, + { + "description": "invalid redirects to else and fails", + "data": "invalid", + "valid": false } ] }, { - "description": "not.json: double negation", + "description": "if-then-else.json: then: false fails when condition matches", "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "not": { - "not": {} - } + "if": { + "const": 1 + }, + "then": false }, "tests": [ { - "description": "any value is valid", - "data": "foo", + "description": "matches if → then=false → invalid", + "data": 1, + "valid": false + }, + { + "description": "does not match if → then ignored → valid", + "data": 2, "valid": true } ] }, { - "description": "oneOf.json: oneOf", + "description": "if-then-else.json: else: false fails when condition does not match", "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - { - "type": "integer" - }, - { - "minimum": 2 - } - ] + "if": { + "const": 1 + }, + "else": false }, "tests": [ { - "description": "first oneOf valid", + "description": "matches if → else ignored → valid", "data": 1, "valid": true }, { - "description": "second oneOf valid", - "data": 2.5, - "valid": true - }, - { - "description": "both oneOf valid", - "data": 3, - "valid": false - }, - { - "description": "neither oneOf valid", - "data": 1.5, + "description": "does not match if → else executes → invalid", + "data": 2, "valid": false } ] }, { - "description": "oneOf.json: oneOf with base schema", + "description": "infinite-loop-detection.json: evaluating the same schema location against the same data location twice is not a sign of an infinite loop", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "string", - "oneOf": [ + "$defs": { + "int": { + "type": "integer" + } + }, + "allOf": [ { - "minLength": 2 + "properties": { + "foo": { + "$ref": "#/$defs/int" + } + } }, { - "maxLength": 4 + "additionalProperties": { + "$ref": "#/$defs/int" + } } ] }, "tests": [ { - "description": "mismatch base schema", - "data": 3, - "valid": false - }, - { - "description": "one oneOf valid", - "data": "foobar", + "description": "passing case", + "data": { + "foo": 1 + }, "valid": true }, { - "description": "both oneOf valid", - "data": "foo", + "description": "failing case", + "data": { + "foo": "a string" + }, "valid": false } ] }, { - "description": "oneOf.json: oneOf with boolean schemas, all true", + "description": "items.json: a schema given for items", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - true, - true, - true - ] + "items": { + "type": "integer" + } }, "tests": [ { - "description": "any value is invalid", - "data": "foo", + "description": "valid items", + "data": [ + 1, + 2, + 3 + ], + "valid": true + }, + { + "description": "wrong type of items", + "data": [ + 1, + "x" + ], "valid": false + }, + { + "description": "ignores non-arrays", + "data": { + "foo": "bar" + }, + "valid": true + }, + { + "description": "JavaScript pseudo-array is valid", + "data": { + "0": "invalid", + "length": 1 + }, + "valid": true } ] }, { - "description": "oneOf.json: oneOf with boolean schemas, one true", + "description": "items.json: items with boolean schema (true)", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - true, - false, - false - ] + "items": true }, "tests": [ { - "description": "any value is valid", - "data": "foo", + "description": "any array is valid", + "data": [ + 1, + "foo", + true + ], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], "valid": true } ] }, { - "description": "oneOf.json: oneOf with boolean schemas, more than one true", + "description": "items.json: items with boolean schema (false)", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - true, - true, - false - ] + "items": false }, "tests": [ { - "description": "any value is invalid", - "data": "foo", + "description": "any non-empty array is invalid", + "data": [ + 1, + "foo", + true + ], "valid": false + }, + { + "description": "empty array is valid", + "data": [], + "valid": true } ] }, { - "description": "oneOf.json: oneOf with boolean schemas, all false", + "description": "items.json: nested items", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - false, - false, - false - ] + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "number" + } + } + } + } }, "tests": [ { - "description": "any value is invalid", - "data": "foo", - "valid": false - } - ] - }, - { - "description": "oneOf.json: oneOf complex types", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - { - "properties": { - "bar": { - "type": "integer" - } - }, - "required": [ - "bar" + "description": "valid nested array", + "data": [ + [ + [ + [ + 1 + ] + ], + [ + [ + 2 + ], + [ + 3 + ] + ] + ], + [ + [ + [ + 4 + ], + [ + 5 + ], + [ + 6 + ] + ] ] - }, - { - "properties": { - "foo": { - "type": "string" + ], + "valid": true + }, + { + "description": "nested array with invalid type", + "data": [ + [ + [ + [ + "1" + ] + ], + [ + [ + 2 + ], + [ + 3 + ] + ] + ], + [ + [ + [ + 4 + ], + [ + 5 + ], + [ + 6 + ] + ] + ] + ], + "valid": false + }, + { + "description": "not deep enough", + "data": [ + [ + [ + 1 + ], + [ + 2 + ], + [ + 3 + ] + ], + [ + [ + 4 + ], + [ + 5 + ], + [ + 6 + ] + ] + ], + "valid": false + } + ] + }, + { + "description": "items.json: items with null instance elements", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { + "type": "null" + } + }, + "tests": [ + { + "description": "allows null elements", + "data": [ + null + ], + "valid": true + } + ] + }, + { + "description": "items.json: items and subitems", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "item": { + "type": "array", + "items": false, + "prefixItems": [ + { + "$ref": "#/$defs/sub-item" + }, + { + "$ref": "#/$defs/sub-item" } - }, + ] + }, + "sub-item": { + "type": "object", "required": [ "foo" ] } + }, + "type": "array", + "items": false, + "prefixItems": [ + { + "$ref": "#/$defs/item" + }, + { + "$ref": "#/$defs/item" + }, + { + "$ref": "#/$defs/item" + } ] }, "tests": [ { - "description": "first oneOf valid (complex)", - "data": { - "bar": 2 - }, - "valid": true + "description": "valid items", + "data": [ + [ + { + "foo": null + }, + { + "foo": null + } + ], + [ + { + "foo": null + }, + { + "foo": null + } + ], + [ + { + "foo": null + }, + { + "foo": null + } + ] + ], + "valid": true + }, + { + "description": "too many items", + "data": [ + [ + { + "foo": null + }, + { + "foo": null + } + ], + [ + { + "foo": null + }, + { + "foo": null + } + ], + [ + { + "foo": null + }, + { + "foo": null + } + ], + [ + { + "foo": null + }, + { + "foo": null + } + ] + ], + "valid": false + }, + { + "description": "too many sub-items", + "data": [ + [ + { + "foo": null + }, + { + "foo": null + }, + { + "foo": null + } + ], + [ + { + "foo": null + }, + { + "foo": null + } + ], + [ + { + "foo": null + }, + { + "foo": null + } + ] + ], + "valid": false + }, + { + "description": "wrong item", + "data": [ + { + "foo": null + }, + [ + { + "foo": null + }, + { + "foo": null + } + ], + [ + { + "foo": null + }, + { + "foo": null + } + ] + ], + "valid": false + }, + { + "description": "wrong sub-item", + "data": [ + [ + {}, + { + "foo": null + } + ], + [ + { + "foo": null + }, + { + "foo": null + } + ], + [ + { + "foo": null + }, + { + "foo": null + } + ] + ], + "valid": false + }, + { + "description": "fewer items is valid", + "data": [ + [ + { + "foo": null + } + ], + [ + { + "foo": null + } + ] + ], + "valid": true + } + ] + }, + { + "description": "items.json: prefixItems with no additional items allowed", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [ + {}, + {}, + {} + ], + "items": false + }, + "tests": [ + { + "description": "empty array", + "data": [], + "valid": true + }, + { + "description": "fewer number of items present (1)", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "fewer number of items present (2)", + "data": [ + 1, + 2 + ], + "valid": true + }, + { + "description": "equal number of items present", + "data": [ + 1, + 2, + 3 + ], + "valid": true + }, + { + "description": "additional items are not permitted", + "data": [ + 1, + 2, + 3, + 4 + ], + "valid": false + } + ] + }, + { + "description": "items.json: items does not look in applicators, valid case", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "prefixItems": [ + { + "minimum": 3 + } + ] + } + ], + "items": { + "minimum": 5 + } + }, + "tests": [ + { + "description": "prefixItems in allOf does not constrain items, invalid case", + "data": [ + 3, + 5 + ], + "valid": false + }, + { + "description": "prefixItems in allOf does not constrain items, valid case", + "data": [ + 5, + 5 + ], + "valid": true + } + ] + }, + { + "description": "items.json: prefixItems validation adjusts the starting index for items", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [ + { + "type": "string" + } + ], + "items": { + "type": "integer" + } + }, + "tests": [ + { + "description": "valid items", + "data": [ + "x", + 2, + 3 + ], + "valid": true + }, + { + "description": "wrong type of second item", + "data": [ + "x", + "y" + ], + "valid": false + } + ] + }, + { + "description": "items.json: items with heterogeneous array", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [ + {} + ], + "items": false + }, + "tests": [ + { + "description": "heterogeneous invalid instance", + "data": [ + "foo", + "bar", + 37 + ], + "valid": false + }, + { + "description": "valid instance", + "data": [ + null + ], + "valid": true + } + ] + }, + { + "description": "maxContains.json: maxContains without contains is ignored", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxContains": 1 + }, + "tests": [ + { + "description": "one item valid against lone maxContains", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "two items still valid against lone maxContains", + "data": [ + 1, + 2 + ], + "valid": true + } + ] + }, + { + "description": "maxContains.json: maxContains with contains", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": { + "const": 1 + }, + "maxContains": 1 + }, + "tests": [ + { + "description": "empty data", + "data": [], + "valid": false + }, + { + "description": "all elements match, valid maxContains", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "all elements match, invalid maxContains", + "data": [ + 1, + 1 + ], + "valid": false + }, + { + "description": "some elements match, valid maxContains", + "data": [ + 1, + 2 + ], + "valid": true + }, + { + "description": "some elements match, invalid maxContains", + "data": [ + 1, + 2, + 1 + ], + "valid": false + } + ] + }, + { + "description": "maxContains.json: maxContains with contains, value with a decimal", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": { + "const": 1 + }, + "maxContains": 1.0 + }, + "tests": [ + { + "description": "one element matches, valid maxContains", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "too many elements match, invalid maxContains", + "data": [ + 1, + 1 + ], + "valid": false + } + ] + }, + { + "description": "maxContains.json: minContains < maxContains", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": { + "const": 1 + }, + "minContains": 1, + "maxContains": 3 + }, + "tests": [ + { + "description": "actual < minContains < maxContains", + "data": [], + "valid": false + }, + { + "description": "minContains < actual < maxContains", + "data": [ + 1, + 1 + ], + "valid": true + }, + { + "description": "minContains < maxContains < actual", + "data": [ + 1, + 1, + 1, + 1 + ], + "valid": false + } + ] + }, + { + "description": "maxContains.json: maxContains = 0 with minContains = 0", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": { + "const": 1 + }, + "minContains": 0, + "maxContains": 0 + }, + "tests": [ + { + "description": "empty array", + "data": [], + "valid": true + }, + { + "description": "one matching item", + "data": [ + 1 + ], + "valid": false + } + ] + }, + { + "description": "maxItems.json: maxItems validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxItems": 2 + }, + "tests": [ + { + "description": "shorter is valid", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "exact length is valid", + "data": [ + 1, + 2 + ], + "valid": true + }, + { + "description": "too long is invalid", + "data": [ + 1, + 2, + 3 + ], + "valid": false + }, + { + "description": "ignores non-arrays", + "data": "foobar", + "valid": true + } + ] + }, + { + "description": "maxItems.json: maxItems validation with a decimal", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxItems": 2.0 + }, + "tests": [ + { + "description": "shorter is valid", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "too long is invalid", + "data": [ + 1, + 2, + 3 + ], + "valid": false + } + ] + }, + { + "description": "maxLength.json: maxLength validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxLength": 2 + }, + "tests": [ + { + "description": "shorter is valid", + "data": "f", + "valid": true + }, + { + "description": "exact length is valid", + "data": "fo", + "valid": true + }, + { + "description": "too long is invalid", + "data": "foo", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + }, + { + "description": "two graphemes is long enough", + "data": "💩💩", + "valid": true + } + ] + }, + { + "description": "maxLength.json: maxLength validation with a decimal", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxLength": 2.0 + }, + "tests": [ + { + "description": "shorter is valid", + "data": "f", + "valid": true + }, + { + "description": "too long is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "maxProperties.json: maxProperties validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxProperties": 2 + }, + "tests": [ + { + "description": "shorter is valid", + "data": { + "foo": 1 + }, + "valid": true + }, + { + "description": "exact length is valid", + "data": { + "foo": 1, + "bar": 2 + }, + "valid": true + }, + { + "description": "too long is invalid", + "data": { + "foo": 1, + "bar": 2, + "baz": 3 + }, + "valid": false + }, + { + "description": "ignores arrays", + "data": [ + 1, + 2, + 3 + ], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobar", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "maxProperties.json: maxProperties validation with a decimal", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxProperties": 2.0 + }, + "tests": [ + { + "description": "shorter is valid", + "data": { + "foo": 1 + }, + "valid": true + }, + { + "description": "too long is invalid", + "data": { + "foo": 1, + "bar": 2, + "baz": 3 + }, + "valid": false + } + ] + }, + { + "description": "maxProperties.json: maxProperties = 0 means the object is empty", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maxProperties": 0 + }, + "tests": [ + { + "description": "no properties is valid", + "data": {}, + "valid": true + }, + { + "description": "one property is invalid", + "data": { + "foo": 1 + }, + "valid": false + } + ] + }, + { + "description": "maximum.json: maximum validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maximum": 3.0 + }, + "tests": [ + { + "description": "below the maximum is valid", + "data": 2.6, + "valid": true + }, + { + "description": "boundary point is valid", + "data": 3.0, + "valid": true + }, + { + "description": "above the maximum is invalid", + "data": 3.5, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + }, + { + "description": "maximum.json: maximum validation with unsigned integer", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "maximum": 300 + }, + "tests": [ + { + "description": "below the maximum is valid", + "data": 299.97, + "valid": true + }, + { + "description": "boundary point integer is valid", + "data": 300, + "valid": true + }, + { + "description": "boundary point float is valid", + "data": 300.0, + "valid": true + }, + { + "description": "above the maximum is invalid", + "data": 300.5, + "valid": false + } + ] + }, + { + "description": "minContains.json: minContains without contains is ignored", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minContains": 1 + }, + "tests": [ + { + "description": "one item valid against lone minContains", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "zero items still valid against lone minContains", + "data": [], + "valid": true + } + ] + }, + { + "description": "minContains.json: minContains=1 with contains", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": { + "const": 1 + }, + "minContains": 1 + }, + "tests": [ + { + "description": "empty data", + "data": [], + "valid": false + }, + { + "description": "no elements match", + "data": [ + 2 + ], + "valid": false + }, + { + "description": "single element matches, valid minContains", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "some elements match, valid minContains", + "data": [ + 1, + 2 + ], + "valid": true + }, + { + "description": "all elements match, valid minContains", + "data": [ + 1, + 1 + ], + "valid": true + } + ] + }, + { + "description": "minContains.json: minContains=2 with contains", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": { + "const": 1 + }, + "minContains": 2 + }, + "tests": [ + { + "description": "empty data", + "data": [], + "valid": false + }, + { + "description": "all elements match, invalid minContains", + "data": [ + 1 + ], + "valid": false + }, + { + "description": "some elements match, invalid minContains", + "data": [ + 1, + 2 + ], + "valid": false + }, + { + "description": "all elements match, valid minContains (exactly as needed)", + "data": [ + 1, + 1 + ], + "valid": true + }, + { + "description": "all elements match, valid minContains (more than needed)", + "data": [ + 1, + 1, + 1 + ], + "valid": true + }, + { + "description": "some elements match, valid minContains", + "data": [ + 1, + 2, + 1 + ], + "valid": true + } + ] + }, + { + "description": "minContains.json: minContains=2 with contains with a decimal value", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": { + "const": 1 + }, + "minContains": 2.0 + }, + "tests": [ + { + "description": "one element matches, invalid minContains", + "data": [ + 1 + ], + "valid": false + }, + { + "description": "both elements match, valid minContains", + "data": [ + 1, + 1 + ], + "valid": true + } + ] + }, + { + "description": "minContains.json: maxContains = minContains", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": { + "const": 1 + }, + "maxContains": 2, + "minContains": 2 + }, + "tests": [ + { + "description": "empty data", + "data": [], + "valid": false + }, + { + "description": "all elements match, invalid minContains", + "data": [ + 1 + ], + "valid": false + }, + { + "description": "all elements match, invalid maxContains", + "data": [ + 1, + 1, + 1 + ], + "valid": false + }, + { + "description": "all elements match, valid maxContains and minContains", + "data": [ + 1, + 1 + ], + "valid": true + } + ] + }, + { + "description": "minContains.json: maxContains < minContains", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": { + "const": 1 + }, + "maxContains": 1, + "minContains": 3 + }, + "tests": [ + { + "description": "empty data", + "data": [], + "valid": false + }, + { + "description": "invalid minContains", + "data": [ + 1 + ], + "valid": false + }, + { + "description": "invalid maxContains", + "data": [ + 1, + 1, + 1 + ], + "valid": false + }, + { + "description": "invalid maxContains and minContains", + "data": [ + 1, + 1 + ], + "valid": false + } + ] + }, + { + "description": "minContains.json: minContains = 0", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": { + "const": 1 + }, + "minContains": 0 + }, + "tests": [ + { + "description": "empty data", + "data": [], + "valid": true + }, + { + "description": "minContains = 0 makes contains always pass", + "data": [ + 2 + ], + "valid": true + } + ] + }, + { + "description": "minContains.json: minContains = 0 with maxContains", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "contains": { + "const": 1 + }, + "minContains": 0, + "maxContains": 1 + }, + "tests": [ + { + "description": "empty data", + "data": [], + "valid": true + }, + { + "description": "not more than maxContains", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "too many", + "data": [ + 1, + 1 + ], + "valid": false + } + ] + }, + { + "description": "minItems.json: minItems validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minItems": 1 + }, + "tests": [ + { + "description": "longer is valid", + "data": [ + 1, + 2 + ], + "valid": true + }, + { + "description": "exact length is valid", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "too short is invalid", + "data": [], + "valid": false + }, + { + "description": "ignores non-arrays", + "data": "", + "valid": true + } + ] + }, + { + "description": "minItems.json: minItems validation with a decimal", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minItems": 1.0 + }, + "tests": [ + { + "description": "longer is valid", + "data": [ + 1, + 2 + ], + "valid": true + }, + { + "description": "too short is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "minLength.json: minLength validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minLength": 2 + }, + "tests": [ + { + "description": "longer is valid", + "data": "foo", + "valid": true + }, + { + "description": "exact length is valid", + "data": "fo", + "valid": true + }, + { + "description": "too short is invalid", + "data": "f", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 1, + "valid": true + }, + { + "description": "one grapheme is not long enough", + "data": "💩", + "valid": false + } + ] + }, + { + "description": "minLength.json: minLength validation with a decimal", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minLength": 2.0 + }, + "tests": [ + { + "description": "longer is valid", + "data": "foo", + "valid": true + }, + { + "description": "too short is invalid", + "data": "f", + "valid": false + } + ] + }, + { + "description": "minProperties.json: minProperties validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minProperties": 1 + }, + "tests": [ + { + "description": "longer is valid", + "data": { + "foo": 1, + "bar": 2 + }, + "valid": true + }, + { + "description": "exact length is valid", + "data": { + "foo": 1 + }, + "valid": true + }, + { + "description": "too short is invalid", + "data": {}, + "valid": false + }, + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores strings", + "data": "", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + }, + { + "description": "ignores null", + "data": null, + "valid": true + }, + { + "description": "ignores booleans", + "data": true, + "valid": true + } + ] + }, + { + "description": "minProperties.json: minProperties validation with a decimal", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minProperties": 1.0 + }, + "tests": [ + { + "description": "longer is valid", + "data": { + "foo": 1, + "bar": 2 + }, + "valid": true + }, + { + "description": "too short is invalid", + "data": {}, + "valid": false + } + ] + }, + { + "description": "minimum.json: minimum validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minimum": 1.1 + }, + "tests": [ + { + "description": "above the minimum is valid", + "data": 2.6, + "valid": true + }, + { + "description": "boundary point is valid", + "data": 1.1, + "valid": true + }, + { + "description": "below the minimum is invalid", + "data": 0.6, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + }, + { + "description": "minimum.json: minimum validation with signed integer", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "minimum": -2 + }, + "tests": [ + { + "description": "negative above the minimum is valid", + "data": -1, + "valid": true + }, + { + "description": "positive above the minimum is valid", + "data": 0, + "valid": true + }, + { + "description": "boundary point is valid", + "data": -2, + "valid": true + }, + { + "description": "boundary point with float is valid", + "data": -2.0, + "valid": true + }, + { + "description": "float below the minimum is invalid", + "data": -2.0001, + "valid": false + }, + { + "description": "int below the minimum is invalid", + "data": -3, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + }, + { + "description": "multipleOf.json: by int", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "multipleOf": 2 + }, + "tests": [ + { + "description": "int by int", + "data": 10, + "valid": true + }, + { + "description": "int by int fail", + "data": 7, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "multipleOf.json: by number", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "multipleOf": 1.5 + }, + "tests": [ + { + "description": "zero is multiple of anything", + "data": 0, + "valid": true + }, + { + "description": "4.5 is multiple of 1.5", + "data": 4.5, + "valid": true + }, + { + "description": "-4.5 is multiple of 1.5", + "data": -4.5, + "valid": true + }, + { + "description": "35 is not multiple of 1.5", + "data": 35, + "valid": false + } + ] + }, + { + "description": "multipleOf.json: by small number", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "multipleOf": 0.0001 + }, + "tests": [ + { + "description": "0.0075 is multiple of 0.0001", + "data": 0.0075, + "valid": true + }, + { + "description": "0.00751 is not multiple of 0.0001", + "data": 0.00751, + "valid": false + } + ] + }, + { + "description": "multipleOf.json: float division = inf", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer", + "multipleOf": 0.123456789 + }, + "tests": [ + { + "description": "always invalid, but naive implementations may raise an overflow error", + "data": 1e+308, + "valid": false + } + ] + }, + { + "description": "multipleOf.json: small multiple of large integer", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer", + "multipleOf": 1e-08 + }, + "tests": [ + { + "description": "any integer is a multiple of 1e-8", + "data": 12391239123, + "valid": true + } + ] + }, + { + "description": "not.json: not", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { + "type": "integer" + } + }, + "tests": [ + { + "description": "allowed", + "data": "foo", + "valid": true + }, + { + "description": "disallowed", + "data": 1, + "valid": false + } + ] + }, + { + "description": "not.json: not multiple types", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { + "type": [ + "integer", + "boolean" + ] + } + }, + "tests": [ + { + "description": "valid", + "data": "foo", + "valid": true + }, + { + "description": "mismatch", + "data": 1, + "valid": false + }, + { + "description": "other mismatch", + "data": true, + "valid": false + } + ] + }, + { + "description": "not.json: not more complex schema", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } + }, + "tests": [ + { + "description": "match", + "data": 1, + "valid": true + }, + { + "description": "other match", + "data": { + "foo": 1 + }, + "valid": true + }, + { + "description": "mismatch", + "data": { + "foo": "bar" + }, + "valid": false + } + ] + }, + { + "description": "not.json: forbidden property", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "not": {} + } + } + }, + "tests": [ + { + "description": "property present", + "data": { + "foo": 1, + "bar": 2 + }, + "valid": false + }, + { + "description": "property absent", + "data": { + "bar": 1, + "baz": 2 + }, + "valid": true + } + ] + }, + { + "description": "not.json: forbid everything with empty schema", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": {} + }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": { + "foo": "bar" + }, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": [ + "foo" + ], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "not.json: forbid everything with boolean schema true", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": true + }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": { + "foo": "bar" + }, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": [ + "foo" + ], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "not.json: allow everything with boolean schema false", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": false + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "boolean true is valid", + "data": true, + "valid": true + }, + { + "description": "boolean false is valid", + "data": false, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "object is valid", + "data": { + "foo": "bar" + }, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "array is valid", + "data": [ + "foo" + ], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "not.json: double negation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "not": { + "not": {} + } + }, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "oneOf.json: oneOf", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "integer" + }, + { + "minimum": 2 + } + ] + }, + "tests": [ + { + "description": "first oneOf valid", + "data": 1, + "valid": true + }, + { + "description": "second oneOf valid", + "data": 2.5, + "valid": true + }, + { + "description": "both oneOf valid", + "data": 3, + "valid": false + }, + { + "description": "neither oneOf valid", + "data": 1.5, + "valid": false + } + ] + }, + { + "description": "oneOf.json: oneOf with base schema", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "oneOf": [ + { + "minLength": 2 + }, + { + "maxLength": 4 + } + ] + }, + "tests": [ + { + "description": "mismatch base schema", + "data": 3, + "valid": false + }, + { + "description": "one oneOf valid", + "data": "foobar", + "valid": true + }, + { + "description": "both oneOf valid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf.json: oneOf with boolean schemas, all true", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + true, + true, + true + ] + }, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf.json: oneOf with boolean schemas, one true", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + true, + false, + false + ] + }, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "oneOf.json: oneOf with boolean schemas, more than one true", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + true, + true, + false + ] + }, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf.json: oneOf with boolean schemas, all false", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + false, + false, + false + ] + }, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf.json: oneOf complex types", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "properties": { + "bar": { + "type": "integer" + } + }, + "required": [ + "bar" + ] + }, + { + "properties": { + "foo": { + "type": "string" + } + }, + "required": [ + "foo" + ] + } + ] + }, + "tests": [ + { + "description": "first oneOf valid (complex)", + "data": { + "bar": 2 + }, + "valid": true + }, + { + "description": "second oneOf valid (complex)", + "data": { + "foo": "baz" + }, + "valid": true + }, + { + "description": "both oneOf valid (complex)", + "data": { + "foo": "baz", + "bar": 2 + }, + "valid": false + }, + { + "description": "neither oneOf valid (complex)", + "data": { + "foo": 2, + "bar": "quux" + }, + "valid": false + } + ] + }, + { + "description": "oneOf.json: oneOf with empty schema", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "type": "number" + }, + {} + ] + }, + "tests": [ + { + "description": "one valid - valid", + "data": "foo", + "valid": true + }, + { + "description": "both valid - invalid", + "data": 123, + "valid": false + } + ] + }, + { + "description": "oneOf.json: oneOf with required", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "oneOf": [ + { + "required": [ + "foo", + "bar" + ] + }, + { + "required": [ + "foo", + "baz" + ] + } + ] + }, + "tests": [ + { + "description": "both invalid - invalid", + "data": { + "bar": 2 + }, + "valid": false + }, + { + "description": "first valid - valid", + "data": { + "foo": 1, + "bar": 2 + }, + "valid": true + }, + { + "description": "second valid - valid", + "data": { + "foo": 1, + "baz": 3 + }, + "valid": true + }, + { + "description": "both valid - invalid", + "data": { + "foo": 1, + "bar": 2, + "baz": 3 + }, + "valid": false + } + ] + }, + { + "description": "oneOf.json: oneOf with missing optional property", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "properties": { + "bar": true, + "baz": true + }, + "required": [ + "bar" + ] + }, + { + "properties": { + "foo": true + }, + "required": [ + "foo" + ] + } + ] + }, + "tests": [ + { + "description": "first oneOf valid", + "data": { + "bar": 8 + }, + "valid": true + }, + { + "description": "second oneOf valid", + "data": { + "foo": "foo" + }, + "valid": true + }, + { + "description": "both oneOf valid", + "data": { + "foo": "foo", + "bar": 8 + }, + "valid": false + }, + { + "description": "neither oneOf valid", + "data": { + "baz": "quux" + }, + "valid": false + } + ] + }, + { + "description": "oneOf.json: nested oneOf, to check validation semantics", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "oneOf": [ + { + "oneOf": [ + { + "type": "null" + } + ] + } + ] + }, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "anything non-null is invalid", + "data": 123, + "valid": false + } + ] + }, + { + "description": "pattern.json: pattern validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "pattern": "^a*$" + }, + "tests": [ + { + "description": "a matching pattern is valid", + "data": "aaa", + "valid": true + }, + { + "description": "a non-matching pattern is invalid", + "data": "abc", + "valid": false + }, + { + "description": "ignores booleans", + "data": true, + "valid": true + }, + { + "description": "ignores integers", + "data": 123, + "valid": true + }, + { + "description": "ignores floats", + "data": 1.0, + "valid": true + }, + { + "description": "ignores objects", + "data": {}, + "valid": true + }, + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores null", + "data": null, + "valid": true + } + ] + }, + { + "description": "pattern.json: pattern is not anchored", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "pattern": "a+" + }, + "tests": [ + { + "description": "matches a substring", + "data": "xxaayy", + "valid": true + } + ] + }, + { + "description": "pattern.json: pattern with Unicode property escape requires unicode mode", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string", + "pattern": "^\\p{Letter}+$" + }, + "tests": [ + { + "description": "ASCII letters match", + "data": "Hello", + "valid": true + }, + { + "description": "Non-ASCII letters match", + "data": "π", + "valid": true + }, + { + "description": "Digits do not match", + "data": "123", + "valid": false + } + ] + }, + { + "description": "patternProperties.json: patternProperties validates properties matching a regex", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "patternProperties": { + "f.*o": { + "type": "integer" + } + } + }, + "tests": [ + { + "description": "a single valid match is valid", + "data": { + "foo": 1 + }, + "valid": true + }, + { + "description": "multiple valid matches is valid", + "data": { + "foo": 1, + "foooooo": 2 + }, + "valid": true + }, + { + "description": "a single invalid match is invalid", + "data": { + "foo": "bar", + "fooooo": 2 + }, + "valid": false + }, + { + "description": "multiple invalid matches is invalid", + "data": { + "foo": "bar", + "foooooo": "baz" + }, + "valid": false + }, + { + "description": "ignores arrays", + "data": [ + "foo" + ], + "valid": true + }, + { + "description": "ignores strings", + "data": "foo", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "patternProperties.json: multiple simultaneous patternProperties are validated", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "patternProperties": { + "a*": { + "type": "integer" + }, + "aaa*": { + "maximum": 20 + } + } + }, + "tests": [ + { + "description": "a single valid match is valid", + "data": { + "a": 21 + }, + "valid": true + }, + { + "description": "a simultaneous match is valid", + "data": { + "aaaa": 18 + }, + "valid": true + }, + { + "description": "multiple matches is valid", + "data": { + "a": 21, + "aaaa": 18 + }, + "valid": true + }, + { + "description": "an invalid due to one is invalid", + "data": { + "a": "bar" + }, + "valid": false + }, + { + "description": "an invalid due to the other is invalid", + "data": { + "aaaa": 31 + }, + "valid": false + }, + { + "description": "an invalid due to both is invalid", + "data": { + "aaa": "foo", + "aaaa": 31 + }, + "valid": false + } + ] + }, + { + "description": "patternProperties.json: regexes are not anchored by default and are case sensitive", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "patternProperties": { + "[0-9]{2,}": { + "type": "boolean" + }, + "X_": { + "type": "string" + } + } + }, + "tests": [ + { + "description": "non recognized members are ignored", + "data": { + "answer 1": "42" + }, + "valid": true + }, + { + "description": "recognized members are accounted for", + "data": { + "a31b": null + }, + "valid": false + }, + { + "description": "regexes are case sensitive", + "data": { + "a_x_3": 3 + }, + "valid": true + }, + { + "description": "regexes are case sensitive, 2", + "data": { + "a_X_3": 3 + }, + "valid": false + } + ] + }, + { + "description": "patternProperties.json: patternProperties with boolean schemas", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "patternProperties": { + "f.*": true, + "b.*": false + } + }, + "tests": [ + { + "description": "object with property matching schema true is valid", + "data": { + "foo": 1 + }, + "valid": true + }, + { + "description": "object with property matching schema false is invalid", + "data": { + "bar": 2 + }, + "valid": false + }, + { + "description": "object with both properties is invalid", + "data": { + "foo": 1, + "bar": 2 + }, + "valid": false + }, + { + "description": "object with a property matching both true and false is invalid", + "data": { + "foobar": 1 + }, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "patternProperties.json: patternProperties with null valued instance properties", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "patternProperties": { + "^.*bar$": { + "type": "null" + } + } + }, + "tests": [ + { + "description": "allows null values", + "data": { + "foobar": null + }, + "valid": true + } + ] + }, + { + "description": "patternProperties.json: patternProperties with Unicode property escape", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^\\p{Letter}+$": { + "type": "number" + } + } + }, + "tests": [ + { + "description": "Unicode letter property name matches", + "data": { + "π": 1 + }, + "valid": true + }, + { + "description": "Non-letter property name does not match pattern", + "data": { + "123": 1 + }, + "valid": true + } + ] + }, + { + "description": "prefixItems.json: a schema given for prefixItems", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "tests": [ + { + "description": "correct types", + "data": [ + 1, + "foo" + ], + "valid": true + }, + { + "description": "wrong types", + "data": [ + "foo", + 1 + ], + "valid": false + }, + { + "description": "incomplete array of items", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "array with additional items", + "data": [ + 1, + "foo", + true + ], + "valid": true + }, + { + "description": "empty array", + "data": [], + "valid": true + }, + { + "description": "JavaScript pseudo-array is valid", + "data": { + "0": "invalid", + "1": "valid", + "length": 2 + }, + "valid": true + } + ] + }, + { + "description": "prefixItems.json: prefixItems with boolean schemas", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [ + true, + false + ] + }, + "tests": [ + { + "description": "array with one item is valid", + "data": [ + 1 + ], + "valid": true + }, + { + "description": "array with two items is invalid", + "data": [ + 1, + "foo" + ], + "valid": false + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "prefixItems.json: additional items are allowed by default", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [ + { + "type": "integer" + } + ] + }, + "tests": [ + { + "description": "only the first item is validated", + "data": [ + 1, + "foo", + false + ], + "valid": true + } + ] + }, + { + "description": "prefixItems.json: prefixItems with null instance elements", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "prefixItems": [ + { + "type": "null" + } + ] + }, + "tests": [ + { + "description": "allows null elements", + "data": [ + null + ], + "valid": true + } + ] + }, + { + "description": "properties.json: object properties validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "integer" + }, + "bar": { + "type": "string" + } + } + }, + "tests": [ + { + "description": "both properties present and valid is valid", + "data": { + "foo": 1, + "bar": "baz" + }, + "valid": true + }, + { + "description": "one property invalid is invalid", + "data": { + "foo": 1, + "bar": {} + }, + "valid": false + }, + { + "description": "both properties invalid is invalid", + "data": { + "foo": [], + "bar": {} + }, + "valid": false + }, + { + "description": "doesn't invalidate other properties", + "data": { + "quux": [] + }, + "valid": true + }, + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "properties.json: properties with boolean schema", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": true, + "bar": false + } + }, + "tests": [ + { + "description": "no property present is valid", + "data": {}, + "valid": true + }, + { + "description": "only 'true' property present is valid", + "data": { + "foo": 1 + }, + "valid": true + }, + { + "description": "only 'false' property present is invalid", + "data": { + "bar": 2 + }, + "valid": false + }, + { + "description": "both properties present is invalid", + "data": { + "foo": 1, + "bar": 2 + }, + "valid": false + } + ] + }, + { + "description": "properties.json: properties with escaped characters", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo\nbar": { + "type": "number" + }, + "foo\"bar": { + "type": "number" + }, + "foo\\bar": { + "type": "number" + }, + "foo\rbar": { + "type": "number" + }, + "foo\tbar": { + "type": "number" + }, + "foo\fbar": { + "type": "number" + } + } + }, + "tests": [ + { + "description": "object with all numbers is valid", + "data": { + "foo\nbar": 1, + "foo\"bar": 1, + "foo\\bar": 1, + "foo\rbar": 1, + "foo\tbar": 1, + "foo\fbar": 1 + }, + "valid": true + }, + { + "description": "object with strings is invalid", + "data": { + "foo\nbar": "1", + "foo\"bar": "1", + "foo\\bar": "1", + "foo\rbar": "1", + "foo\tbar": "1", + "foo\fbar": "1" + }, + "valid": false + } + ] + }, + { + "description": "properties.json: properties with null valued instance properties", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "null" + } + } + }, + "tests": [ + { + "description": "allows null values", + "data": { + "foo": null + }, + "valid": true + } + ] + }, + { + "description": "properties.json: properties whose names are Javascript object property names", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "__proto__": { + "type": "number" + }, + "toString": { + "properties": { + "length": { + "type": "string" + } + } + }, + "constructor": { + "type": "number" + } + } + }, + "tests": [ + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + }, + { + "description": "none of the properties mentioned", + "data": {}, + "valid": true + }, + { + "description": "__proto__ not valid", + "data": { + "__proto__": "foo" + }, + "valid": false + }, + { + "description": "toString not valid", + "data": { + "toString": { + "length": 37 + } + }, + "valid": false + }, + { + "description": "constructor not valid", + "data": { + "constructor": { + "length": 37 + } + }, + "valid": false + }, + { + "description": "all present and valid", + "data": { + "__proto__": 12, + "toString": { + "length": "foo" + }, + "constructor": 37 + }, + "valid": true + } + ] + }, + { + "description": "propertyNames.json: propertyNames validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "propertyNames": { + "maxLength": 3 + } + }, + "tests": [ + { + "description": "all property names valid", + "data": { + "f": {}, + "foo": {} + }, + "valid": true + }, + { + "description": "some property names invalid", + "data": { + "foo": {}, + "foobar": {} + }, + "valid": false + }, + { + "description": "object without properties is valid", + "data": {}, + "valid": true + }, + { + "description": "ignores arrays", + "data": [ + 1, + 2, + 3, + 4 + ], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobar", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + }, + { + "description": "ignores null", + "data": null, + "valid": true + }, + { + "description": "ignores booleans", + "data": true, + "valid": true + } + ] + }, + { + "description": "propertyNames.json: propertyNames validation with pattern", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "propertyNames": { + "pattern": "^a+$" + } + }, + "tests": [ + { + "description": "matching property names valid", + "data": { + "a": {}, + "aa": {}, + "aaa": {} + }, + "valid": true + }, + { + "description": "non-matching property name is invalid", + "data": { + "aaA": {} + }, + "valid": false + }, + { + "description": "object without properties is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames.json: propertyNames with boolean schema true", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "propertyNames": true + }, + "tests": [ + { + "description": "object with any properties is valid", + "data": { + "foo": 1 + }, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames.json: propertyNames with boolean schema false", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "propertyNames": false + }, + "tests": [ + { + "description": "object with any properties is invalid", + "data": { + "foo": 1 + }, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames.json: propertyNames with const", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "propertyNames": { + "const": "foo" + } + }, + "tests": [ + { + "description": "object with property foo is valid", + "data": { + "foo": 1 + }, + "valid": true + }, + { + "description": "object with any other property is invalid", + "data": { + "bar": 1 + }, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames.json: propertyNames with enum", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "propertyNames": { + "enum": [ + "foo", + "bar" + ] + } + }, + "tests": [ + { + "description": "object with property foo is valid", + "data": { + "foo": 1 + }, + "valid": true + }, + { + "description": "object with property foo and bar is valid", + "data": { + "foo": 1, + "bar": 1 + }, + "valid": true + }, + { + "description": "object with any other property is invalid", + "data": { + "baz": 1 + }, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "ref.json: root pointer ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "$ref": "#" + } + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "match", + "data": { + "foo": false + }, + "valid": true + }, + { + "description": "recursive match", + "data": { + "foo": { + "foo": false + } + }, + "valid": true + }, + { + "description": "mismatch", + "data": { + "bar": false + }, + "valid": false + }, + { + "description": "recursive mismatch", + "data": { + "foo": { + "bar": false + } + }, + "valid": false + } + ] + }, + { + "description": "ref.json: relative pointer ref to object", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "integer" + }, + "bar": { + "$ref": "#/properties/foo" + } + } + }, + "tests": [ + { + "description": "match", + "data": { + "bar": 3 + }, + "valid": true + }, + { + "description": "mismatch", + "data": { + "bar": true + }, + "valid": false + } + ] + }, + { + "description": "ref.json: escaped pointer ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "tilde~field": { + "type": "integer" + }, + "slash/field": { + "type": "integer" + }, + "percent%field": { + "type": "integer" + } + }, + "properties": { + "tilde": { + "$ref": "#/$defs/tilde~0field" + }, + "slash": { + "$ref": "#/$defs/slash~1field" + }, + "percent": { + "$ref": "#/$defs/percent%25field" + } + } + }, + "tests": [ + { + "description": "slash invalid", + "data": { + "slash": "aoeu" + }, + "valid": false + }, + { + "description": "tilde invalid", + "data": { + "tilde": "aoeu" + }, + "valid": false + }, + { + "description": "percent invalid", + "data": { + "percent": "aoeu" + }, + "valid": false + }, + { + "description": "slash valid", + "data": { + "slash": 123 + }, + "valid": true + }, + { + "description": "tilde valid", + "data": { + "tilde": 123 + }, + "valid": true + }, + { + "description": "percent valid", + "data": { + "percent": 123 + }, + "valid": true + } + ] + }, + { + "description": "ref.json: nested refs", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "a": { + "type": "integer" + }, + "b": { + "$ref": "#/$defs/a" + }, + "c": { + "$ref": "#/$defs/b" + } + }, + "$ref": "#/$defs/c" + }, + "tests": [ + { + "description": "nested ref valid", + "data": 5, + "valid": true + }, + { + "description": "nested ref invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "ref.json: ref applies alongside sibling keywords", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "reffed": { + "type": "array" + } + }, + "properties": { + "foo": { + "$ref": "#/$defs/reffed", + "maxItems": 2 + } + } + }, + "tests": [ + { + "description": "ref valid, maxItems valid", + "data": { + "foo": [] + }, + "valid": true + }, + { + "description": "ref valid, maxItems invalid", + "data": { + "foo": [ + 1, + 2, + 3 + ] + }, + "valid": false + }, + { + "description": "ref invalid", + "data": { + "foo": "string" + }, + "valid": false + } + ] + }, + { + "description": "ref.json: property named $ref that is not a reference", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "$ref": { + "type": "string" + } + } + }, + "tests": [ + { + "description": "property named $ref valid", + "data": { + "$ref": "a" + }, + "valid": true + }, + { + "description": "property named $ref invalid", + "data": { + "$ref": 2 + }, + "valid": false + } + ] + }, + { + "description": "ref.json: property named $ref, containing an actual $ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "$ref": { + "$ref": "#/$defs/is-string" + } + }, + "$defs": { + "is-string": { + "type": "string" + } + } + }, + "tests": [ + { + "description": "property named $ref valid", + "data": { + "$ref": "a" + }, + "valid": true + }, + { + "description": "property named $ref invalid", + "data": { + "$ref": 2 + }, + "valid": false + } + ] + }, + { + "description": "ref.json: $ref to boolean schema true", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "#/$defs/bool", + "$defs": { + "bool": true + } + }, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "ref.json: $ref to boolean schema false", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "#/$defs/bool", + "$defs": { + "bool": false + } + }, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "ref.json: Recursive references between schemas", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://localhost:1234/draft2020-12/tree", + "description": "tree of nodes", + "type": "object", + "properties": { + "meta": { + "type": "string" + }, + "nodes": { + "type": "array", + "items": { + "$ref": "node" + } + } + }, + "required": [ + "meta", + "nodes" + ], + "$defs": { + "node": { + "$id": "http://localhost:1234/draft2020-12/node", + "description": "node", + "type": "object", + "properties": { + "value": { + "type": "number" + }, + "subtree": { + "$ref": "tree" + } + }, + "required": [ + "value" + ] + } + } + }, + "tests": [ + { + "description": "valid tree", + "data": { + "meta": "root", + "nodes": [ + { + "value": 1, + "subtree": { + "meta": "child", + "nodes": [ + { + "value": 1.1 + }, + { + "value": 1.2 + } + ] + } + }, + { + "value": 2, + "subtree": { + "meta": "child", + "nodes": [ + { + "value": 2.1 + }, + { + "value": 2.2 + } + ] + } + } + ] + }, + "valid": true + }, + { + "description": "invalid tree", + "data": { + "meta": "root", + "nodes": [ + { + "value": 1, + "subtree": { + "meta": "child", + "nodes": [ + { + "value": "string is invalid" + }, + { + "value": 1.2 + } + ] + } + }, + { + "value": 2, + "subtree": { + "meta": "child", + "nodes": [ + { + "value": 2.1 + }, + { + "value": 2.2 + } + ] + } + } + ] + }, + "valid": false + } + ] + }, + { + "description": "ref.json: refs with quote", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo\"bar": { + "$ref": "#/$defs/foo%22bar" + } + }, + "$defs": { + "foo\"bar": { + "type": "number" + } + } + }, + "tests": [ + { + "description": "object with numbers is valid", + "data": { + "foo\"bar": 1 + }, + "valid": true + }, + { + "description": "object with strings is invalid", + "data": { + "foo\"bar": "1" + }, + "valid": false + } + ] + }, + { + "description": "ref.json: naive replacement of $ref with its destination is not correct", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "a_string": { + "type": "string" + } + }, + "enum": [ + { + "$ref": "#/$defs/a_string" + } + ] + }, + "tests": [ + { + "description": "do not evaluate the $ref inside the enum, matching any string", + "data": "this is a string", + "valid": false + }, + { + "description": "do not evaluate the $ref inside the enum, definition exact match", + "data": { + "type": "string" + }, + "valid": false + }, + { + "description": "match the enum exactly", + "data": { + "$ref": "#/$defs/a_string" + }, + "valid": true + } + ] + }, + { + "description": "ref.json: refs with relative uris and defs", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://example.com/schema-relative-uri-defs1.json", + "properties": { + "foo": { + "$id": "schema-relative-uri-defs2.json", + "$defs": { + "inner": { + "properties": { + "bar": { + "type": "string" + } + } + } + }, + "$ref": "#/$defs/inner" + } + }, + "$ref": "schema-relative-uri-defs2.json" + }, + "tests": [ + { + "description": "invalid on inner field", + "data": { + "foo": { + "bar": 1 + }, + "bar": "a" + }, + "valid": false + }, + { + "description": "invalid on outer field", + "data": { + "foo": { + "bar": "a" + }, + "bar": 1 + }, + "valid": false + }, + { + "description": "valid on both fields", + "data": { + "foo": { + "bar": "a" + }, + "bar": "a" + }, + "valid": true + } + ] + }, + { + "description": "ref.json: relative refs with absolute uris and defs", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://example.com/schema-refs-absolute-uris-defs1.json", + "properties": { + "foo": { + "$id": "http://example.com/schema-refs-absolute-uris-defs2.json", + "$defs": { + "inner": { + "properties": { + "bar": { + "type": "string" + } + } + } + }, + "$ref": "#/$defs/inner" + } + }, + "$ref": "schema-refs-absolute-uris-defs2.json" + }, + "tests": [ + { + "description": "invalid on inner field", + "data": { + "foo": { + "bar": 1 + }, + "bar": "a" + }, + "valid": false + }, + { + "description": "invalid on outer field", + "data": { + "foo": { + "bar": "a" + }, + "bar": 1 + }, + "valid": false + }, + { + "description": "valid on both fields", + "data": { + "foo": { + "bar": "a" + }, + "bar": "a" + }, + "valid": true + } + ] + }, + { + "description": "ref.json: $id must be resolved against nearest parent, not just immediate parent", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://example.com/a.json", + "$defs": { + "x": { + "$id": "http://example.com/b/c.json", + "not": { + "$defs": { + "y": { + "$id": "d.json", + "type": "number" + } + } + } + } + }, + "allOf": [ + { + "$ref": "http://example.com/b/d.json" + } + ] + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "ref.json: order of evaluation: $id and $ref", + "schema": { + "$comment": "$id must be evaluated before $ref to get the proper $ref destination", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/draft2020-12/ref-and-id1/base.json", + "$ref": "int.json", + "$defs": { + "bigint": { + "$comment": "canonical uri: https://example.com/ref-and-id1/int.json", + "$id": "int.json", + "maximum": 10 + }, + "smallint": { + "$comment": "canonical uri: https://example.com/ref-and-id1-int.json", + "$id": "/draft2020-12/ref-and-id1-int.json", + "maximum": 2 + } + } + }, + "tests": [ + { + "description": "data is valid against first definition", + "data": 5, + "valid": true + }, + { + "description": "data is invalid against first definition", + "data": 50, + "valid": false + } + ] + }, + { + "description": "ref.json: order of evaluation: $id and $anchor and $ref", + "schema": { + "$comment": "$id must be evaluated before $ref to get the proper $ref destination", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/draft2020-12/ref-and-id2/base.json", + "$ref": "#bigint", + "$defs": { + "bigint": { + "$comment": "canonical uri: /ref-and-id2/base.json#/$defs/bigint; another valid uri for this location: /ref-and-id2/base.json#bigint", + "$anchor": "bigint", + "maximum": 10 + }, + "smallint": { + "$comment": "canonical uri: https://example.com/ref-and-id2#/$defs/smallint; another valid uri for this location: https://example.com/ref-and-id2/#bigint", + "$id": "https://example.com/draft2020-12/ref-and-id2/", + "$anchor": "bigint", + "maximum": 2 + } + } + }, + "tests": [ + { + "description": "data is valid against first definition", + "data": 5, + "valid": true + }, + { + "description": "data is invalid against first definition", + "data": 50, + "valid": false + } + ] + }, + { + "description": "ref.json: order of evaluation: $id and $ref on nested schema", + "schema": { + "$comment": "$id must be evaluated before $ref to get the proper $ref destination", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/draft2020-12/ref-and-id3/base.json", + "$ref": "nested/foo.json", + "$defs": { + "foo": { + "$comment": "canonical uri: https://example.com/draft2020-12/ref-and-id3/nested/foo.json", + "$id": "nested/foo.json", + "$ref": "./bar.json" + }, + "bar": { + "$comment": "canonical uri: https://example.com/draft2020-12/ref-and-id3/nested/bar.json", + "$id": "nested/bar.json", + "type": "number" + } + } + }, + "tests": [ + { + "description": "data is valid against nested sibling", + "data": 5, + "valid": true + }, + { + "description": "data is invalid against nested sibling", + "data": "a", + "valid": false + } + ] + }, + { + "description": "ref.json: simple URN base URI with $ref via the URN", + "schema": { + "$comment": "URIs do not have to have HTTP(s) schemes", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed", + "minimum": 30, + "properties": { + "foo": { + "$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed" + } + } + }, + "tests": [ + { + "description": "valid under the URN IDed schema", + "data": { + "foo": 37 + }, + "valid": true + }, + { + "description": "invalid under the URN IDed schema", + "data": { + "foo": 12 + }, + "valid": false + } + ] + }, + { + "description": "ref.json: simple URN base URI with JSON pointer", + "schema": { + "$comment": "URIs do not have to have HTTP(s) schemes", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:uuid:deadbeef-1234-00ff-ff00-4321feebdaed", + "properties": { + "foo": { + "$ref": "#/$defs/bar" + } + }, + "$defs": { + "bar": { + "type": "string" + } + } + }, + "tests": [ + { + "description": "a string is valid", + "data": { + "foo": "bar" + }, + "valid": true + }, + { + "description": "a non-string is invalid", + "data": { + "foo": 12 + }, + "valid": false + } + ] + }, + { + "description": "ref.json: URN base URI with NSS", + "schema": { + "$comment": "RFC 8141 §2.2", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:example:1/406/47452/2", + "properties": { + "foo": { + "$ref": "#/$defs/bar" + } + }, + "$defs": { + "bar": { + "type": "string" + } + } + }, + "tests": [ + { + "description": "a string is valid", + "data": { + "foo": "bar" + }, + "valid": true + }, + { + "description": "a non-string is invalid", + "data": { + "foo": 12 + }, + "valid": false + } + ] + }, + { + "description": "ref.json: URN base URI with r-component", + "schema": { + "$comment": "RFC 8141 §2.3.1", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:example:foo-bar-baz-qux?+CCResolve:cc=uk", + "properties": { + "foo": { + "$ref": "#/$defs/bar" + } + }, + "$defs": { + "bar": { + "type": "string" + } + } + }, + "tests": [ + { + "description": "a string is valid", + "data": { + "foo": "bar" + }, + "valid": true + }, + { + "description": "a non-string is invalid", + "data": { + "foo": 12 + }, + "valid": false + } + ] + }, + { + "description": "ref.json: URN base URI with q-component", + "schema": { + "$comment": "RFC 8141 §2.3.2", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:example:weather?=op=map&lat=39.56&lon=-104.85&datetime=1969-07-21T02:56:15Z", + "properties": { + "foo": { + "$ref": "#/$defs/bar" + } + }, + "$defs": { + "bar": { + "type": "string" + } + } + }, + "tests": [ + { + "description": "a string is valid", + "data": { + "foo": "bar" + }, + "valid": true + }, + { + "description": "a non-string is invalid", + "data": { + "foo": 12 + }, + "valid": false + } + ] + }, + { + "description": "ref.json: URN base URI with URN and JSON pointer ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:uuid:deadbeef-1234-0000-0000-4321feebdaed", + "properties": { + "foo": { + "$ref": "urn:uuid:deadbeef-1234-0000-0000-4321feebdaed#/$defs/bar" + } + }, + "$defs": { + "bar": { + "type": "string" + } + } + }, + "tests": [ + { + "description": "a string is valid", + "data": { + "foo": "bar" + }, + "valid": true + }, + { + "description": "a non-string is invalid", + "data": { + "foo": 12 + }, + "valid": false + } + ] + }, + { + "description": "ref.json: URN base URI with URN and anchor ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:uuid:deadbeef-1234-ff00-00ff-4321feebdaed", + "properties": { + "foo": { + "$ref": "urn:uuid:deadbeef-1234-ff00-00ff-4321feebdaed#something" + } + }, + "$defs": { + "bar": { + "$anchor": "something", + "type": "string" + } + } + }, + "tests": [ + { + "description": "a string is valid", + "data": { + "foo": "bar" + }, + "valid": true + }, + { + "description": "a non-string is invalid", + "data": { + "foo": 12 + }, + "valid": false + } + ] + }, + { + "description": "ref.json: URN ref with nested pointer ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", + "$defs": { + "foo": { + "$id": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", + "$defs": { + "bar": { + "type": "string" + } + }, + "$ref": "#/$defs/bar" + } + } + }, + "tests": [ + { + "description": "a string is valid", + "data": "bar", + "valid": true + }, + { + "description": "a non-string is invalid", + "data": 12, + "valid": false + } + ] + }, + { + "description": "ref.json: ref with absolute-path-reference", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://example.com/ref/absref.json", + "$defs": { + "a": { + "$id": "http://example.com/ref/absref/foobar.json", + "type": "number" + }, + "b": { + "$id": "http://example.com/absref/foobar.json", + "type": "string" + } + }, + "$ref": "/absref/foobar.json" + }, + "tests": [ + { + "description": "a string is valid", + "data": "foo", + "valid": true + }, + { + "description": "an integer is invalid", + "data": 12, + "valid": false + } + ] + }, + { + "description": "ref.json: $id with file URI still resolves pointers - *nix", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "file:///folder/file.json", + "$defs": { + "foo": { + "type": "number" + } + }, + "$ref": "#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "ref.json: $id with file URI still resolves pointers - windows", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "file:///c:/folder/file.json", + "$defs": { + "foo": { + "type": "number" + } + }, + "$ref": "#/$defs/foo" + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "ref.json: empty tokens in $ref json-pointer", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "": { + "$defs": { + "": { + "type": "number" + } + } + } + }, + "allOf": [ + { + "$ref": "#/$defs//$defs/" + } + ] + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "refRemote.json: remote ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/draft2020-12/integer.json" + }, + "documents": { + "http://localhost:1234/draft2020-12/integer.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + } + }, + "tests": [ + { + "description": "remote ref valid", + "data": 1, + "valid": true + }, + { + "description": "remote ref invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "refRemote.json: fragment within remote ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/integer" + }, + "documents": { + "http://localhost:1234/draft2020-12/subSchemas.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } + } + } + }, + "tests": [ + { + "description": "remote fragment valid", + "data": 1, + "valid": true + }, + { + "description": "remote fragment invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "refRemote.json: anchor within remote ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/draft2020-12/locationIndependentIdentifier.json#foo" + }, + "documents": { + "http://localhost:1234/draft2020-12/locationIndependentIdentifier.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "refToInteger": { + "$ref": "#foo" + }, + "A": { + "$anchor": "foo", + "type": "integer" + } + } + } + }, + "tests": [ + { + "description": "remote anchor valid", + "data": 1, + "valid": true + }, + { + "description": "remote anchor invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "refRemote.json: ref within remote ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/refToInteger" + }, + "documents": { + "http://localhost:1234/draft2020-12/subSchemas.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "integer": { + "type": "integer" + }, + "refToInteger": { + "$ref": "#/$defs/integer" + } + } + } + }, + "tests": [ + { + "description": "ref within ref valid", + "data": 1, + "valid": true + }, + { + "description": "ref within ref invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "refRemote.json: base URI change", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://localhost:1234/draft2020-12/", + "items": { + "$id": "baseUriChange/", + "items": { + "$ref": "folderInteger.json" + } + } + }, + "documents": { + "http://localhost:1234/draft2020-12/baseUriChange/folderInteger.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + } + }, + "tests": [ + { + "description": "base URI change ref valid", + "data": [ + [ + 1 + ] + ], + "valid": true + }, + { + "description": "base URI change ref invalid", + "data": [ + [ + "a" + ] + ], + "valid": false + } + ] + }, + { + "description": "refRemote.json: base URI change - change folder", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://localhost:1234/draft2020-12/scope_change_defs1.json", + "type": "object", + "properties": { + "list": { + "$ref": "baseUriChangeFolder/" + } + }, + "$defs": { + "baz": { + "$id": "baseUriChangeFolder/", + "type": "array", + "items": { + "$ref": "folderInteger.json" + } + } + } + }, + "documents": { + "http://localhost:1234/draft2020-12/baseUriChangeFolder/folderInteger.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + } + }, + "tests": [ + { + "description": "number is valid", + "data": { + "list": [ + 1 + ] + }, + "valid": true + }, + { + "description": "string is invalid", + "data": { + "list": [ + "a" + ] + }, + "valid": false + } + ] + }, + { + "description": "refRemote.json: base URI change - change folder in subschema", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://localhost:1234/draft2020-12/scope_change_defs2.json", + "type": "object", + "properties": { + "list": { + "$ref": "baseUriChangeFolderInSubschema/#/$defs/bar" + } + }, + "$defs": { + "baz": { + "$id": "baseUriChangeFolderInSubschema/", + "$defs": { + "bar": { + "type": "array", + "items": { + "$ref": "folderInteger.json" + } + } + } + } + } + }, + "documents": { + "http://localhost:1234/draft2020-12/baseUriChangeFolderInSubschema/folderInteger.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + } + }, + "tests": [ + { + "description": "number is valid", + "data": { + "list": [ + 1 + ] + }, + "valid": true + }, + { + "description": "string is invalid", + "data": { + "list": [ + "a" + ] + }, + "valid": false + } + ] + }, + { + "description": "refRemote.json: root ref in remote ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://localhost:1234/draft2020-12/object", + "type": "object", + "properties": { + "name": { + "$ref": "name-defs.json#/$defs/orNull" + } + } + }, + "documents": { + "http://localhost:1234/draft2020-12/name-defs.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "orNull": { + "anyOf": [ + { + "type": "null" + }, + { + "$ref": "#" + } + ] + } + }, + "type": "string" + } + }, + "tests": [ + { + "description": "string is valid", + "data": { + "name": "foo" + }, + "valid": true + }, + { + "description": "null is valid", + "data": { + "name": null + }, + "valid": true + }, + { + "description": "object is invalid", + "data": { + "name": { + "name": null + } + }, + "valid": false + } + ] + }, + { + "description": "refRemote.json: remote ref with ref to defs", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://localhost:1234/draft2020-12/schema-remote-ref-ref-defs1.json", + "$ref": "ref-and-defs.json" + }, + "documents": { + "http://localhost:1234/draft2020-12/ref-and-defs.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://localhost:1234/draft2020-12/ref-and-defs.json", + "$defs": { + "inner": { + "properties": { + "bar": { + "type": "string" + } + } + } + }, + "$ref": "#/$defs/inner" + } + }, + "tests": [ + { + "description": "invalid", + "data": { + "bar": 1 + }, + "valid": false + }, + { + "description": "valid", + "data": { + "bar": "a" + }, + "valid": true + } + ] + }, + { + "description": "refRemote.json: Location-independent identifier in remote ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/draft2020-12/locationIndependentIdentifier.json#/$defs/refToInteger" + }, + "documents": { + "http://localhost:1234/draft2020-12/locationIndependentIdentifier.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "refToInteger": { + "$ref": "#foo" + }, + "A": { + "$anchor": "foo", + "type": "integer" + } + } + } + }, + "tests": [ + { + "description": "integer is valid", + "data": 1, + "valid": true + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "refRemote.json: retrieved nested refs resolve relative to their URI not $id", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "http://localhost:1234/draft2020-12/some-id", + "properties": { + "name": { + "$ref": "nested/foo-ref-string.json" + } + } + }, + "documents": { + "http://localhost:1234/draft2020-12/nested/foo-ref-string.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { + "$ref": "string.json" + } + } + }, + "http://localhost:1234/draft2020-12/nested/string.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" + } + }, + "tests": [ + { + "description": "number is invalid", + "data": { + "name": { + "foo": 1 + } + }, + "valid": false + }, + { + "description": "string is valid", + "data": { + "name": { + "foo": "a" + } + }, + "valid": true + } + ] + }, + { + "description": "refRemote.json: remote HTTP ref with different $id", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/draft2020-12/different-id-ref-string.json" + }, + "documents": { + "http://localhost:1234/draft2020-12/different-id-ref-string.json": { + "$id": "http://localhost:1234/draft2020-12/real-id-ref-string.json", + "$defs": { + "bar": { + "type": "string" + } + }, + "$ref": "#/$defs/bar" + } + }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "refRemote.json: remote HTTP ref with different URN $id", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/draft2020-12/urn-ref-string.json" + }, + "documents": { + "http://localhost:1234/draft2020-12/urn-ref-string.json": { + "$id": "urn:uuid:feebdaed-ffff-0000-2020-1200deadbeef", + "$defs": { + "bar": { + "type": "string" + } + }, + "$ref": "#/$defs/bar" + } + }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "refRemote.json: remote HTTP ref with nested absolute ref", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/draft2020-12/nested-absolute-ref-to-string.json" + }, + "documents": { + "http://localhost:1234/draft2020-12/nested-absolute-ref-to-string.json": { + "$defs": { + "bar": { + "$id": "http://localhost:1234/draft2020-12/the-nested-id.json", + "type": "string" + } + }, + "$ref": "http://localhost:1234/draft2020-12/the-nested-id.json" + } + }, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "refRemote.json: $ref to $ref finds detached $anchor", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "http://localhost:1234/draft2020-12/detached-ref.json#/$defs/foo" + }, + "documents": { + "http://localhost:1234/draft2020-12/detached-ref.json": { + "$id": "http://localhost:1234/draft2020-12/detached-ref.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$defs": { + "foo": { + "$ref": "#detached" + }, + "detached": { + "$anchor": "detached", + "type": "integer" + } + } + } + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "non-number is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "required.json: required validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": {}, + "bar": {} + }, + "required": [ + "foo" + ] + }, + "tests": [ + { + "description": "present required property is valid", + "data": { + "foo": 1 + }, + "valid": true + }, + { + "description": "non-present required property is invalid", + "data": { + "bar": 1 + }, + "valid": false + }, + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores strings", + "data": "", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + }, + { + "description": "ignores null", + "data": null, + "valid": true + }, + { + "description": "ignores boolean", + "data": true, + "valid": true + } + ] + }, + { + "description": "required.json: required default validation", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": {} + } + }, + "tests": [ + { + "description": "not required by default", + "data": {}, + "valid": true + } + ] + }, + { + "description": "required.json: required with empty array", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": {} + }, + "required": [] + }, + "tests": [ + { + "description": "property not required", + "data": {}, + "valid": true + } + ] + }, + { + "description": "required.json: required with escaped characters", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "required": [ + "foo\nbar", + "foo\"bar", + "foo\\bar", + "foo\rbar", + "foo\tbar", + "foo\fbar" + ] + }, + "tests": [ + { + "description": "object with all properties present is valid", + "data": { + "foo\nbar": 1, + "foo\"bar": 1, + "foo\\bar": 1, + "foo\rbar": 1, + "foo\tbar": 1, + "foo\fbar": 1 + }, + "valid": true + }, + { + "description": "object with some properties missing is invalid", + "data": { + "foo\nbar": "1", + "foo\"bar": "1" + }, + "valid": false + } + ] + }, + { + "description": "required.json: required properties whose names are Javascript object property names", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "required": [ + "__proto__", + "toString", + "constructor" + ] + }, + "tests": [ + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + }, + { + "description": "none of the properties mentioned", + "data": {}, + "valid": false + }, + { + "description": "__proto__ present", + "data": { + "__proto__": "foo" + }, + "valid": false + }, + { + "description": "toString present", + "data": { + "toString": { + "length": 37 + } + }, + "valid": false + }, + { + "description": "constructor present", + "data": { + "constructor": { + "length": 37 + } + }, + "valid": false + }, + { + "description": "all present", + "data": { + "__proto__": 12, + "toString": { + "length": "foo" + }, + "constructor": 37 + }, + "valid": true + } + ] + }, + { + "description": "type.json: integer type matches integers", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "integer" + }, + "tests": [ + { + "description": "an integer is an integer", + "data": 1, + "valid": true + }, + { + "description": "a float with zero fractional part is an integer", + "data": 1.0, + "valid": true + }, + { + "description": "a float is not an integer", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an integer", + "data": "foo", + "valid": false + }, + { + "description": "a string is still not an integer, even if it looks like one", + "data": "1", + "valid": false }, { - "description": "second oneOf valid (complex)", - "data": { - "foo": "baz" - }, - "valid": true + "description": "an object is not an integer", + "data": {}, + "valid": false }, { - "description": "both oneOf valid (complex)", - "data": { - "foo": "baz", - "bar": 2 - }, + "description": "an array is not an integer", + "data": [], "valid": false }, { - "description": "neither oneOf valid (complex)", - "data": { - "foo": 2, - "bar": "quux" - }, + "description": "a boolean is not an integer", + "data": true, + "valid": false + }, + { + "description": "null is not an integer", + "data": null, "valid": false } ] }, { - "description": "oneOf.json: oneOf with empty schema", + "description": "type.json: number type matches numbers", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - { - "type": "number" - }, - {} - ] + "type": "number" }, "tests": [ { - "description": "one valid - valid", - "data": "foo", + "description": "an integer is a number", + "data": 1, "valid": true }, { - "description": "both valid - invalid", - "data": 123, + "description": "a float with zero fractional part is a number (and an integer)", + "data": 1.0, + "valid": true + }, + { + "description": "a float is a number", + "data": 1.1, + "valid": true + }, + { + "description": "a string is not a number", + "data": "foo", + "valid": false + }, + { + "description": "a string is still not a number, even if it looks like one", + "data": "1", + "valid": false + }, + { + "description": "an object is not a number", + "data": {}, + "valid": false + }, + { + "description": "an array is not a number", + "data": [], + "valid": false + }, + { + "description": "a boolean is not a number", + "data": true, + "valid": false + }, + { + "description": "null is not a number", + "data": null, "valid": false } ] }, { - "description": "oneOf.json: oneOf with required", + "description": "type.json: string type matches strings", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "oneOf": [ - { - "required": [ - "foo", - "bar" - ] - }, - { - "required": [ - "foo", - "baz" - ] - } - ] + "type": "string" }, "tests": [ { - "description": "both invalid - invalid", - "data": { - "bar": 2 - }, + "description": "1 is not a string", + "data": 1, "valid": false }, { - "description": "first valid - valid", - "data": { - "foo": 1, - "bar": 2 - }, + "description": "a float is not a string", + "data": 1.1, + "valid": false + }, + { + "description": "a string is a string", + "data": "foo", "valid": true }, { - "description": "second valid - valid", - "data": { - "foo": 1, - "baz": 3 - }, + "description": "a string is still a string, even if it looks like a number", + "data": "1", "valid": true }, { - "description": "both valid - invalid", - "data": { - "foo": 1, - "bar": 2, - "baz": 3 - }, + "description": "an empty string is still a string", + "data": "", + "valid": true + }, + { + "description": "an object is not a string", + "data": {}, + "valid": false + }, + { + "description": "an array is not a string", + "data": [], + "valid": false + }, + { + "description": "a boolean is not a string", + "data": true, + "valid": false + }, + { + "description": "null is not a string", + "data": null, "valid": false } ] }, { - "description": "oneOf.json: oneOf with missing optional property", + "description": "type.json: object type matches objects", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - { - "properties": { - "bar": true, - "baz": true - }, - "required": [ - "bar" - ] - }, - { - "properties": { - "foo": true - }, - "required": [ - "foo" - ] - } - ] + "type": "object" }, "tests": [ { - "description": "first oneOf valid", - "data": { - "bar": 8 - }, - "valid": true + "description": "an integer is not an object", + "data": 1, + "valid": false }, { - "description": "second oneOf valid", - "data": { - "foo": "foo" - }, + "description": "a float is not an object", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an object", + "data": "foo", + "valid": false + }, + { + "description": "an object is an object", + "data": {}, "valid": true }, { - "description": "both oneOf valid", - "data": { - "foo": "foo", - "bar": 8 - }, + "description": "an array is not an object", + "data": [], "valid": false }, { - "description": "neither oneOf valid", - "data": { - "baz": "quux" - }, + "description": "a boolean is not an object", + "data": true, + "valid": false + }, + { + "description": "null is not an object", + "data": null, "valid": false } ] }, { - "description": "oneOf.json: nested oneOf, to check validation semantics", + "description": "type.json: array type matches arrays", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "oneOf": [ - { - "oneOf": [ - { - "type": "null" - } - ] - } - ] + "type": "array" }, "tests": [ { - "description": "null is valid", - "data": null, + "description": "an integer is not an array", + "data": 1, + "valid": false + }, + { + "description": "a float is not an array", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an array", + "data": "foo", + "valid": false + }, + { + "description": "an object is not an array", + "data": {}, + "valid": false + }, + { + "description": "an array is an array", + "data": [], "valid": true }, { - "description": "anything non-null is invalid", - "data": 123, + "description": "a boolean is not an array", + "data": true, + "valid": false + }, + { + "description": "null is not an array", + "data": null, "valid": false } ] }, { - "description": "pattern.json: pattern validation", + "description": "type.json: boolean type matches booleans", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "pattern": "^a*$" + "type": "boolean" }, "tests": [ { - "description": "a matching pattern is valid", - "data": "aaa", - "valid": true + "description": "an integer is not a boolean", + "data": 1, + "valid": false }, { - "description": "a non-matching pattern is invalid", - "data": "abc", + "description": "zero is not a boolean", + "data": 0, "valid": false }, { - "description": "ignores booleans", - "data": true, - "valid": true + "description": "a float is not a boolean", + "data": 1.1, + "valid": false }, { - "description": "ignores integers", - "data": 123, - "valid": true + "description": "a string is not a boolean", + "data": "foo", + "valid": false }, { - "description": "ignores floats", - "data": 1.0, - "valid": true + "description": "an empty string is not a boolean", + "data": "", + "valid": false }, { - "description": "ignores objects", + "description": "an object is not a boolean", "data": {}, - "valid": true + "valid": false }, { - "description": "ignores arrays", + "description": "an array is not a boolean", "data": [], - "valid": true + "valid": false }, { - "description": "ignores null", - "data": null, + "description": "true is a boolean", + "data": true, "valid": true - } - ] - }, - { - "description": "pattern.json: pattern is not anchored", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "pattern": "a+" - }, - "tests": [ + }, { - "description": "matches a substring", - "data": "xxaayy", + "description": "false is a boolean", + "data": false, "valid": true + }, + { + "description": "null is not a boolean", + "data": null, + "valid": false } ] }, { - "description": "pattern.json: pattern with Unicode property escape requires unicode mode", + "description": "type.json: null type matches only the null object", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "string", - "pattern": "^\\p{Letter}+$" + "type": "null" }, "tests": [ { - "description": "ASCII letters match", - "data": "Hello", - "valid": true + "description": "an integer is not null", + "data": 1, + "valid": false }, { - "description": "Non-ASCII letters match", - "data": "π", - "valid": true + "description": "a float is not null", + "data": 1.1, + "valid": false }, { - "description": "Digits do not match", - "data": "123", + "description": "zero is not null", + "data": 0, "valid": false - } - ] - }, - { - "description": "properties.json: object properties validation", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo": { - "type": "integer" - }, - "bar": { - "type": "string" - } - } - }, - "tests": [ - { - "description": "both properties present and valid is valid", - "data": { - "foo": 1, - "bar": "baz" - }, - "valid": true }, { - "description": "one property invalid is invalid", - "data": { - "foo": 1, - "bar": {} - }, + "description": "a string is not null", + "data": "foo", "valid": false }, { - "description": "both properties invalid is invalid", - "data": { - "foo": [], - "bar": {} - }, + "description": "an empty string is not null", + "data": "", "valid": false }, { - "description": "doesn't invalidate other properties", - "data": { - "quux": [] - }, - "valid": true + "description": "an object is not null", + "data": {}, + "valid": false }, { - "description": "ignores arrays", + "description": "an array is not null", "data": [], - "valid": true + "valid": false }, { - "description": "ignores other non-objects", - "data": 12, + "description": "true is not null", + "data": true, + "valid": false + }, + { + "description": "false is not null", + "data": false, + "valid": false + }, + { + "description": "null is null", + "data": null, "valid": true } ] }, { - "description": "properties.json: properties with boolean schema", + "description": "type.json: multiple types can be specified in an array", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo": true, - "bar": false - } + "type": [ + "integer", + "string" + ] }, "tests": [ { - "description": "no property present is valid", - "data": {}, + "description": "an integer is valid", + "data": 1, "valid": true }, { - "description": "only 'true' property present is valid", - "data": { - "foo": 1 - }, + "description": "a string is valid", + "data": "foo", "valid": true }, { - "description": "only 'false' property present is invalid", - "data": { - "bar": 2 - }, + "description": "a float is invalid", + "data": 1.1, "valid": false }, { - "description": "both properties present is invalid", - "data": { - "foo": 1, - "bar": 2 - }, + "description": "an object is invalid", + "data": {}, "valid": false - } - ] - }, - { - "description": "properties.json: properties with escaped characters", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo\nbar": { - "type": "number" - }, - "foo\"bar": { - "type": "number" - }, - "foo\\bar": { - "type": "number" - }, - "foo\rbar": { - "type": "number" - }, - "foo\tbar": { - "type": "number" - }, - "foo\fbar": { - "type": "number" - } - } - }, - "tests": [ + }, { - "description": "object with all numbers is valid", - "data": { - "foo\nbar": 1, - "foo\"bar": 1, - "foo\\bar": 1, - "foo\rbar": 1, - "foo\tbar": 1, - "foo\fbar": 1 - }, - "valid": true + "description": "an array is invalid", + "data": [], + "valid": false }, { - "description": "object with strings is invalid", - "data": { - "foo\nbar": "1", - "foo\"bar": "1", - "foo\\bar": "1", - "foo\rbar": "1", - "foo\tbar": "1", - "foo\fbar": "1" - }, + "description": "a boolean is invalid", + "data": true, + "valid": false + }, + { + "description": "null is invalid", + "data": null, "valid": false } ] }, { - "description": "properties.json: properties with null valued instance properties", + "description": "type.json: type as array with one item", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo": { - "type": "null" - } - } + "type": [ + "string" + ] }, "tests": [ { - "description": "allows null values", - "data": { - "foo": null - }, + "description": "string is valid", + "data": "foo", "valid": true + }, + { + "description": "number is invalid", + "data": 123, + "valid": false } ] }, { - "description": "properties.json: properties whose names are Javascript object property names", + "description": "type.json: type: array or object", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "__proto__": { - "type": "number" - }, - "toString": { - "properties": { - "length": { - "type": "string" - } - } - }, - "constructor": { - "type": "number" - } - } + "type": [ + "array", + "object" + ] }, "tests": [ { - "description": "ignores arrays", - "data": [], - "valid": true - }, - { - "description": "ignores other non-objects", - "data": 12, - "valid": true - }, - { - "description": "none of the properties mentioned", - "data": {}, + "description": "array is valid", + "data": [ + 1, + 2, + 3 + ], "valid": true }, { - "description": "__proto__ not valid", + "description": "object is valid", "data": { - "__proto__": "foo" + "foo": 123 }, - "valid": false + "valid": true }, { - "description": "toString not valid", - "data": { - "toString": { - "length": 37 - } - }, + "description": "number is invalid", + "data": 123, "valid": false }, { - "description": "constructor not valid", - "data": { - "constructor": { - "length": 37 - } - }, + "description": "string is invalid", + "data": "foo", "valid": false }, { - "description": "all present and valid", - "data": { - "__proto__": 12, - "toString": { - "length": "foo" - }, - "constructor": 37 - }, - "valid": true + "description": "null is invalid", + "data": null, + "valid": false } ] }, { - "description": "propertyNames.json: propertyNames validation", + "description": "type.json: type: array, object or null", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "propertyNames": { - "maxLength": 3 - } + "type": [ + "array", + "object", + "null" + ] }, "tests": [ { - "description": "all property names valid", - "data": { - "f": {}, - "foo": {} - }, + "description": "array is valid", + "data": [ + 1, + 2, + 3 + ], "valid": true }, { - "description": "some property names invalid", + "description": "object is valid", "data": { - "foo": {}, - "foobar": {} + "foo": 123 }, - "valid": false + "valid": true }, { - "description": "object without properties is valid", - "data": {}, + "description": "null is valid", + "data": null, "valid": true }, { - "description": "ignores arrays", - "data": [ - 1, - 2, - 3, - 4 - ], - "valid": true + "description": "number is invalid", + "data": 123, + "valid": false }, { - "description": "ignores strings", - "data": "foobar", + "description": "string is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "unevaluatedItems.json: unevaluatedItems true", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "unevaluatedItems": true + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": [], "valid": true }, { - "description": "ignores other non-objects", - "data": 12, + "description": "with unevaluated items", + "data": [ + "foo" + ], "valid": true - }, + } + ] + }, + { + "description": "unevaluatedItems.json: unevaluatedItems false", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "unevaluatedItems": false + }, + "tests": [ { - "description": "ignores null", - "data": null, + "description": "with no unevaluated items", + "data": [], "valid": true }, { - "description": "ignores booleans", - "data": true, - "valid": true + "description": "with unevaluated items", + "data": [ + "foo" + ], + "valid": false } ] }, { - "description": "propertyNames.json: propertyNames validation with pattern", + "description": "unevaluatedItems.json: unevaluatedItems as schema", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "propertyNames": { - "pattern": "^a+$" + "unevaluatedItems": { + "type": "string" } }, "tests": [ { - "description": "matching property names valid", - "data": { - "a": {}, - "aa": {}, - "aaa": {} - }, + "description": "with no unevaluated items", + "data": [], "valid": true }, { - "description": "non-matching property name is invalid", - "data": { - "aaA": {} - }, + "description": "with valid unevaluated items", + "data": [ + "foo" + ], + "valid": true + }, + { + "description": "with invalid unevaluated items", + "data": [ + 42 + ], "valid": false + } + ] + }, + { + "description": "unevaluatedItems.json: unevaluatedItems with uniform items", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { + "type": "string" }, + "unevaluatedItems": false + }, + "tests": [ { - "description": "object without properties is valid", - "data": {}, + "description": "unevaluatedItems doesn't apply", + "data": [ + "foo", + "bar" + ], "valid": true } ] }, { - "description": "propertyNames.json: propertyNames with boolean schema true", + "description": "unevaluatedItems.json: unevaluatedItems with tuple", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "propertyNames": true + "prefixItems": [ + { + "type": "string" + } + ], + "unevaluatedItems": false }, "tests": [ { - "description": "object with any properties is valid", - "data": { - "foo": 1 - }, + "description": "with no unevaluated items", + "data": [ + "foo" + ], "valid": true }, { - "description": "empty object is valid", - "data": {}, - "valid": true + "description": "with unevaluated items", + "data": [ + "foo", + "bar" + ], + "valid": false } ] }, { - "description": "propertyNames.json: propertyNames with boolean schema false", + "description": "unevaluatedItems.json: unevaluatedItems with items and prefixItems", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "propertyNames": false + "prefixItems": [ + { + "type": "string" + } + ], + "items": true, + "unevaluatedItems": false }, "tests": [ { - "description": "object with any properties is invalid", - "data": { - "foo": 1 - }, - "valid": false - }, - { - "description": "empty object is valid", - "data": {}, + "description": "unevaluatedItems doesn't apply", + "data": [ + "foo", + 42 + ], "valid": true } ] }, { - "description": "propertyNames.json: propertyNames with const", + "description": "unevaluatedItems.json: unevaluatedItems with items", "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "propertyNames": { - "const": "foo" + "$schema": "https://json-schema.org/draft/2020-12/schema", + "items": { + "type": "number" + }, + "unevaluatedItems": { + "type": "string" } }, "tests": [ { - "description": "object with property foo is valid", - "data": { - "foo": 1 - }, + "description": "valid under items", + "comment": "no elements are considered by unevaluatedItems", + "data": [ + 5, + 6, + 7, + 8 + ], "valid": true }, { - "description": "object with any other property is invalid", - "data": { - "bar": 1 - }, + "description": "invalid under items", + "data": [ + "foo", + "bar", + "baz" + ], "valid": false - }, - { - "description": "empty object is valid", - "data": {}, - "valid": true } ] }, { - "description": "propertyNames.json: propertyNames with enum", + "description": "unevaluatedItems.json: unevaluatedItems with nested tuple", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "propertyNames": { - "enum": [ - "foo", - "bar" - ] - } + "prefixItems": [ + { + "type": "string" + } + ], + "allOf": [ + { + "prefixItems": [ + true, + { + "type": "number" + } + ] + } + ], + "unevaluatedItems": false }, "tests": [ { - "description": "object with property foo is valid", - "data": { - "foo": 1 - }, - "valid": true - }, - { - "description": "object with property foo and bar is valid", - "data": { - "foo": 1, - "bar": 1 - }, + "description": "with no unevaluated items", + "data": [ + "foo", + 42 + ], "valid": true }, { - "description": "object with any other property is invalid", - "data": { - "baz": 1 - }, + "description": "with unevaluated items", + "data": [ + "foo", + 42, + true + ], "valid": false - }, - { - "description": "empty object is valid", - "data": {}, - "valid": true } ] }, { - "description": "ref.json: root pointer ref", + "description": "unevaluatedItems.json: unevaluatedItems with nested items", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo": { - "$ref": "#" - } + "unevaluatedItems": { + "type": "boolean" }, - "additionalProperties": false + "anyOf": [ + { + "items": { + "type": "string" + } + }, + true + ] }, "tests": [ { - "description": "match", - "data": { - "foo": false - }, + "description": "with only (valid) additional items", + "data": [ + true, + false + ], "valid": true }, { - "description": "recursive match", - "data": { - "foo": { - "foo": false - } - }, + "description": "with no additional items", + "data": [ + "yes", + "no" + ], "valid": true }, { - "description": "mismatch", - "data": { - "bar": false - }, - "valid": false - }, - { - "description": "recursive mismatch", - "data": { - "foo": { - "bar": false - } - }, + "description": "with invalid additional item", + "data": [ + "yes", + false + ], "valid": false } ] }, { - "description": "ref.json: relative pointer ref to object", + "description": "unevaluatedItems.json: unevaluatedItems with nested prefixItems and items", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo": { - "type": "integer" - }, - "bar": { - "$ref": "#/properties/foo" + "allOf": [ + { + "prefixItems": [ + { + "type": "string" + } + ], + "items": true } - } + ], + "unevaluatedItems": false }, "tests": [ { - "description": "match", - "data": { - "bar": 3 - }, + "description": "with no additional items", + "data": [ + "foo" + ], "valid": true }, { - "description": "mismatch", - "data": { - "bar": true - }, - "valid": false + "description": "with additional items", + "data": [ + "foo", + 42, + true + ], + "valid": true } ] }, { - "description": "ref.json: escaped pointer ref", + "description": "unevaluatedItems.json: unevaluatedItems with nested unevaluatedItems", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "tilde~field": { - "type": "integer" - }, - "slash/field": { - "type": "integer" - }, - "percent%field": { - "type": "integer" - } - }, - "properties": { - "tilde": { - "$ref": "#/$defs/tilde~0field" - }, - "slash": { - "$ref": "#/$defs/slash~1field" + "allOf": [ + { + "prefixItems": [ + { + "type": "string" + } + ] }, - "percent": { - "$ref": "#/$defs/percent%25field" + { + "unevaluatedItems": true } - } + ], + "unevaluatedItems": false }, "tests": [ { - "description": "slash invalid", - "data": { - "slash": "aoeu" - }, - "valid": false - }, - { - "description": "tilde invalid", - "data": { - "tilde": "aoeu" - }, - "valid": false - }, - { - "description": "percent invalid", - "data": { - "percent": "aoeu" - }, - "valid": false - }, - { - "description": "slash valid", - "data": { - "slash": 123 - }, - "valid": true - }, - { - "description": "tilde valid", - "data": { - "tilde": 123 - }, + "description": "with no additional items", + "data": [ + "foo" + ], "valid": true }, { - "description": "percent valid", - "data": { - "percent": 123 - }, + "description": "with additional items", + "data": [ + "foo", + 42, + true + ], "valid": true } ] }, { - "description": "ref.json: nested refs", + "description": "unevaluatedItems.json: unevaluatedItems with anyOf", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "a": { - "type": "integer" - }, - "b": { - "$ref": "#/$defs/a" + "prefixItems": [ + { + "const": "foo" + } + ], + "anyOf": [ + { + "prefixItems": [ + true, + { + "const": "bar" + } + ] }, - "c": { - "$ref": "#/$defs/b" + { + "prefixItems": [ + true, + true, + { + "const": "baz" + } + ] } + ], + "unevaluatedItems": false + }, + "tests": [ + { + "description": "when one schema matches and has no unevaluated items", + "data": [ + "foo", + "bar" + ], + "valid": true + }, + { + "description": "when one schema matches and has unevaluated items", + "data": [ + "foo", + "bar", + 42 + ], + "valid": false }, - "$ref": "#/$defs/c" - }, - "tests": [ { - "description": "nested ref valid", - "data": 5, + "description": "when two schemas match and has no unevaluated items", + "data": [ + "foo", + "bar", + "baz" + ], "valid": true }, { - "description": "nested ref invalid", - "data": "a", + "description": "when two schemas match and has unevaluated items", + "data": [ + "foo", + "bar", + "baz", + 42 + ], "valid": false } ] }, { - "description": "ref.json: ref applies alongside sibling keywords", + "description": "unevaluatedItems.json: unevaluatedItems with oneOf", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "reffed": { - "type": "array" + "prefixItems": [ + { + "const": "foo" } - }, - "properties": { - "foo": { - "$ref": "#/$defs/reffed", - "maxItems": 2 + ], + "oneOf": [ + { + "prefixItems": [ + true, + { + "const": "bar" + } + ] + }, + { + "prefixItems": [ + true, + { + "const": "baz" + } + ] } - } + ], + "unevaluatedItems": false }, "tests": [ { - "description": "ref valid, maxItems valid", - "data": { - "foo": [] - }, + "description": "with no unevaluated items", + "data": [ + "foo", + "bar" + ], "valid": true }, { - "description": "ref valid, maxItems invalid", - "data": { - "foo": [ - 1, - 2, - 3 - ] - }, - "valid": false - }, - { - "description": "ref invalid", - "data": { - "foo": "string" - }, + "description": "with unevaluated items", + "data": [ + "foo", + "bar", + 42 + ], "valid": false } ] }, { - "description": "ref.json: property named $ref that is not a reference", + "description": "unevaluatedItems.json: unevaluatedItems with not", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "$ref": { - "type": "string" + "prefixItems": [ + { + "const": "foo" } - } + ], + "not": { + "not": { + "prefixItems": [ + true, + { + "const": "bar" + } + ] + } + }, + "unevaluatedItems": false }, "tests": [ { - "description": "property named $ref valid", - "data": { - "$ref": "a" - }, - "valid": true - }, - { - "description": "property named $ref invalid", - "data": { - "$ref": 2 - }, + "description": "with unevaluated items", + "data": [ + "foo", + "bar" + ], "valid": false } ] }, { - "description": "ref.json: property named $ref, containing an actual $ref", + "description": "unevaluatedItems.json: unevaluatedItems with if/then/else", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "$ref": { - "$ref": "#/$defs/is-string" + "prefixItems": [ + { + "const": "foo" } + ], + "if": { + "prefixItems": [ + true, + { + "const": "bar" + } + ] }, - "$defs": { - "is-string": { - "type": "string" - } - } + "then": { + "prefixItems": [ + true, + true, + { + "const": "then" + } + ] + }, + "else": { + "prefixItems": [ + true, + true, + true, + { + "const": "else" + } + ] + }, + "unevaluatedItems": false }, "tests": [ { - "description": "property named $ref valid", - "data": { - "$ref": "a" - }, + "description": "when if matches and it has no unevaluated items", + "data": [ + "foo", + "bar", + "then" + ], "valid": true }, { - "description": "property named $ref invalid", - "data": { - "$ref": 2 - }, + "description": "when if matches and it has unevaluated items", + "data": [ + "foo", + "bar", + "then", + "else" + ], "valid": false - } - ] - }, - { - "description": "ref.json: $ref to boolean schema true", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "#/$defs/bool", - "$defs": { - "bool": true - } - }, - "tests": [ + }, { - "description": "any value is valid", - "data": "foo", + "description": "when if doesn't match and it has no unevaluated items", + "data": [ + "foo", + 42, + 42, + "else" + ], "valid": true - } - ] - }, - { - "description": "ref.json: $ref to boolean schema false", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "#/$defs/bool", - "$defs": { - "bool": false - } - }, - "tests": [ + }, { - "description": "any value is invalid", - "data": "foo", + "description": "when if doesn't match and it has unevaluated items", + "data": [ + "foo", + 42, + 42, + "else", + 42 + ], "valid": false } ] }, { - "description": "ref.json: Recursive references between schemas", + "description": "unevaluatedItems.json: unevaluatedItems with boolean schemas", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/tree", - "description": "tree of nodes", - "type": "object", - "properties": { - "meta": { - "type": "string" - }, - "nodes": { - "type": "array", - "items": { - "$ref": "node" - } - } - }, - "required": [ - "meta", - "nodes" + "allOf": [ + true ], - "$defs": { - "node": { - "$id": "http://localhost:1234/draft2020-12/node", - "description": "node", - "type": "object", - "properties": { - "value": { - "type": "number" - }, - "subtree": { - "$ref": "tree" - } - }, - "required": [ - "value" - ] - } - } + "unevaluatedItems": false }, "tests": [ { - "description": "valid tree", - "data": { - "meta": "root", - "nodes": [ - { - "value": 1, - "subtree": { - "meta": "child", - "nodes": [ - { - "value": 1.1 - }, - { - "value": 1.2 - } - ] - } - }, - { - "value": 2, - "subtree": { - "meta": "child", - "nodes": [ - { - "value": 2.1 - }, - { - "value": 2.2 - } - ] - } - } - ] - }, + "description": "with no unevaluated items", + "data": [], "valid": true }, { - "description": "invalid tree", - "data": { - "meta": "root", - "nodes": [ - { - "value": 1, - "subtree": { - "meta": "child", - "nodes": [ - { - "value": "string is invalid" - }, - { - "value": 1.2 - } - ] - } - }, - { - "value": 2, - "subtree": { - "meta": "child", - "nodes": [ - { - "value": 2.1 - }, - { - "value": 2.2 - } - ] - } - } - ] - }, + "description": "with unevaluated items", + "data": [ + "foo" + ], "valid": false } ] }, { - "description": "ref.json: refs with quote", + "description": "unevaluatedItems.json: unevaluatedItems with $ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo\"bar": { - "$ref": "#/$defs/foo%22bar" + "$ref": "#/$defs/bar", + "prefixItems": [ + { + "type": "string" } - }, + ], + "unevaluatedItems": false, "$defs": { - "foo\"bar": { - "type": "number" + "bar": { + "prefixItems": [ + true, + { + "type": "string" + } + ] } } }, "tests": [ { - "description": "object with numbers is valid", - "data": { - "foo\"bar": 1 - }, + "description": "with no unevaluated items", + "data": [ + "foo", + "bar" + ], "valid": true }, { - "description": "object with strings is invalid", - "data": { - "foo\"bar": "1" - }, + "description": "with unevaluated items", + "data": [ + "foo", + "bar", + "baz" + ], "valid": false } ] }, { - "description": "ref.json: naive replacement of $ref with its destination is not correct", + "description": "unevaluatedItems.json: unevaluatedItems before $ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "a_string": { + "unevaluatedItems": false, + "prefixItems": [ + { "type": "string" } - }, - "enum": [ - { - "$ref": "#/$defs/a_string" + ], + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "prefixItems": [ + true, + { + "type": "string" + } + ] } - ] + } }, "tests": [ { - "description": "do not evaluate the $ref inside the enum, matching any string", - "data": "this is a string", - "valid": false + "description": "with no unevaluated items", + "data": [ + "foo", + "bar" + ], + "valid": true }, { - "description": "do not evaluate the $ref inside the enum, definition exact match", - "data": { - "type": "string" - }, + "description": "with unevaluated items", + "data": [ + "foo", + "bar", + "baz" + ], "valid": false - }, - { - "description": "match the enum exactly", - "data": { - "$ref": "#/$defs/a_string" + } + ] + }, + { + "description": "unevaluatedItems.json: unevaluatedItems can't see inside cousins", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "prefixItems": [ + true + ] }, - "valid": true + { + "unevaluatedItems": false + } + ] + }, + "tests": [ + { + "description": "always fails", + "data": [ + 1 + ], + "valid": false } ] }, { - "description": "ref.json: refs with relative uris and defs", + "description": "unevaluatedItems.json: item is evaluated in an uncle schema to unevaluatedItems", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://example.com/schema-relative-uri-defs1.json", "properties": { "foo": { - "$id": "schema-relative-uri-defs2.json", - "$defs": { - "inner": { - "properties": { - "bar": { + "prefixItems": [ + { + "type": "string" + } + ], + "unevaluatedItems": false + } + }, + "anyOf": [ + { + "properties": { + "foo": { + "prefixItems": [ + true, + { "type": "string" } - } + ] } - }, - "$ref": "#/$defs/inner" + } } - }, - "$ref": "schema-relative-uri-defs2.json" + ] }, "tests": [ { - "description": "invalid on inner field", + "description": "no extra items", "data": { - "foo": { - "bar": 1 - }, - "bar": "a" + "foo": [ + "test" + ] }, - "valid": false + "valid": true }, { - "description": "invalid on outer field", + "description": "uncle keyword evaluation is not significant", "data": { - "foo": { - "bar": "a" - }, - "bar": 1 + "foo": [ + "test", + "test" + ] }, "valid": false - }, - { - "description": "valid on both fields", - "data": { - "foo": { - "bar": "a" - }, - "bar": "a" - }, - "valid": true } ] }, { - "description": "ref.json: relative refs with absolute uris and defs", + "description": "unevaluatedItems.json: unevaluatedItems depends on adjacent contains", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://example.com/schema-refs-absolute-uris-defs1.json", - "properties": { - "foo": { - "$id": "http://example.com/schema-refs-absolute-uris-defs2.json", - "$defs": { - "inner": { - "properties": { - "bar": { - "type": "string" - } - } - } - }, - "$ref": "#/$defs/inner" - } + "prefixItems": [ + true + ], + "contains": { + "type": "string" }, - "$ref": "schema-refs-absolute-uris-defs2.json" + "unevaluatedItems": false }, "tests": [ { - "description": "invalid on inner field", - "data": { - "foo": { - "bar": 1 - }, - "bar": "a" - }, - "valid": false + "description": "second item is evaluated by contains", + "data": [ + 1, + "foo" + ], + "valid": true }, { - "description": "invalid on outer field", - "data": { - "foo": { - "bar": "a" - }, - "bar": 1 - }, + "description": "contains fails, second item is not evaluated", + "data": [ + 1, + 2 + ], "valid": false }, { - "description": "valid on both fields", - "data": { - "foo": { - "bar": "a" - }, - "bar": "a" - }, - "valid": true + "description": "contains passes, second item is not evaluated", + "data": [ + 1, + 2, + "foo" + ], + "valid": false } ] }, { - "description": "ref.json: $id must be resolved against nearest parent, not just immediate parent", + "description": "unevaluatedItems.json: unevaluatedItems depends on multiple nested contains", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://example.com/a.json", - "$defs": { - "x": { - "$id": "http://example.com/b/c.json", - "not": { - "$defs": { - "y": { - "$id": "d.json", - "type": "number" - } - } - } - } - }, "allOf": [ { - "$ref": "http://example.com/b/d.json" + "contains": { + "multipleOf": 2 + } + }, + { + "contains": { + "multipleOf": 3 + } } - ] + ], + "unevaluatedItems": { + "multipleOf": 5 + } }, "tests": [ { - "description": "number is valid", - "data": 1, + "description": "5 not evaluated, passes unevaluatedItems", + "data": [ + 2, + 3, + 4, + 5, + 6 + ], "valid": true }, { - "description": "non-number is invalid", - "data": "a", + "description": "7 not evaluated, fails unevaluatedItems", + "data": [ + 2, + 3, + 4, + 7, + 8 + ], "valid": false } ] }, { - "description": "ref.json: order of evaluation: $id and $ref", + "description": "unevaluatedItems.json: unevaluatedItems and contains interact to control item dependency relationship", "schema": { - "$comment": "$id must be evaluated before $ref to get the proper $ref destination", "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://example.com/draft2020-12/ref-and-id1/base.json", - "$ref": "int.json", - "$defs": { - "bigint": { - "$comment": "canonical uri: https://example.com/ref-and-id1/int.json", - "$id": "int.json", - "maximum": 10 + "if": { + "contains": { + "const": "a" + } + }, + "then": { + "if": { + "contains": { + "const": "b" + } }, - "smallint": { - "$comment": "canonical uri: https://example.com/ref-and-id1-int.json", - "$id": "/draft2020-12/ref-and-id1-int.json", - "maximum": 2 + "then": { + "if": { + "contains": { + "const": "c" + } + } } - } + }, + "unevaluatedItems": false }, "tests": [ { - "description": "data is valid against first definition", - "data": 5, + "description": "empty array is valid", + "data": [], "valid": true }, { - "description": "data is invalid against first definition", - "data": 50, + "description": "only a's are valid", + "data": [ + "a", + "a" + ], + "valid": true + }, + { + "description": "a's and b's are valid", + "data": [ + "a", + "b", + "a", + "b", + "a" + ], + "valid": true + }, + { + "description": "a's, b's and c's are valid", + "data": [ + "c", + "a", + "c", + "c", + "b", + "a" + ], + "valid": true + }, + { + "description": "only b's are invalid", + "data": [ + "b", + "b" + ], + "valid": false + }, + { + "description": "only c's are invalid", + "data": [ + "c", + "c" + ], + "valid": false + }, + { + "description": "only b's and c's are invalid", + "data": [ + "c", + "b", + "c", + "b", + "c" + ], + "valid": false + }, + { + "description": "only a's and c's are invalid", + "data": [ + "c", + "a", + "c", + "a", + "c" + ], "valid": false } ] }, { - "description": "ref.json: order of evaluation: $id and $anchor and $ref", + "description": "unevaluatedItems.json: unevaluatedItems with minContains = 0", "schema": { - "$comment": "$id must be evaluated before $ref to get the proper $ref destination", "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://example.com/draft2020-12/ref-and-id2/base.json", - "$ref": "#bigint", - "$defs": { - "bigint": { - "$comment": "canonical uri: /ref-and-id2/base.json#/$defs/bigint; another valid uri for this location: /ref-and-id2/base.json#bigint", - "$anchor": "bigint", - "maximum": 10 - }, - "smallint": { - "$comment": "canonical uri: https://example.com/ref-and-id2#/$defs/smallint; another valid uri for this location: https://example.com/ref-and-id2/#bigint", - "$id": "https://example.com/draft2020-12/ref-and-id2/", - "$anchor": "bigint", - "maximum": 2 - } - } + "contains": { + "type": "string" + }, + "minContains": 0, + "unevaluatedItems": false }, "tests": [ { - "description": "data is valid against first definition", - "data": 5, + "description": "empty array is valid", + "data": [], "valid": true }, { - "description": "data is invalid against first definition", - "data": 50, + "description": "no items evaluated by contains", + "data": [ + 0 + ], + "valid": false + }, + { + "description": "some but not all items evaluated by contains", + "data": [ + "foo", + 0 + ], "valid": false + }, + { + "description": "all items evaluated by contains", + "data": [ + "foo", + "bar" + ], + "valid": true } ] }, { - "description": "ref.json: order of evaluation: $id and $ref on nested schema", + "description": "unevaluatedItems.json: non-array instances are valid", "schema": { - "$comment": "$id must be evaluated before $ref to get the proper $ref destination", "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://example.com/draft2020-12/ref-and-id3/base.json", - "$ref": "nested/foo.json", - "$defs": { - "foo": { - "$comment": "canonical uri: https://example.com/draft2020-12/ref-and-id3/nested/foo.json", - "$id": "nested/foo.json", - "$ref": "./bar.json" - }, - "bar": { - "$comment": "canonical uri: https://example.com/draft2020-12/ref-and-id3/nested/bar.json", - "$id": "nested/bar.json", - "type": "number" - } - } + "unevaluatedItems": false }, "tests": [ { - "description": "data is valid against nested sibling", - "data": 5, + "description": "ignores booleans", + "data": true, "valid": true }, { - "description": "data is invalid against nested sibling", - "data": "a", - "valid": false + "description": "ignores integers", + "data": 123, + "valid": true + }, + { + "description": "ignores floats", + "data": 1.0, + "valid": true + }, + { + "description": "ignores objects", + "data": {}, + "valid": true + }, + { + "description": "ignores strings", + "data": "foo", + "valid": true + }, + { + "description": "ignores null", + "data": null, + "valid": true } ] }, { - "description": "ref.json: simple URN base URI with $ref via the URN", + "description": "unevaluatedItems.json: unevaluatedItems with null instance elements", "schema": { - "$comment": "URIs do not have to have HTTP(s) schemes", "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed", - "minimum": 30, - "properties": { - "foo": { - "$ref": "urn:uuid:deadbeef-1234-ffff-ffff-4321feebdaed" - } + "unevaluatedItems": { + "type": "null" } }, "tests": [ { - "description": "valid under the URN IDed schema", - "data": { - "foo": 37 - }, + "description": "allows null elements", + "data": [ + null + ], "valid": true - }, - { - "description": "invalid under the URN IDed schema", - "data": { - "foo": 12 - }, - "valid": false } ] }, { - "description": "ref.json: simple URN base URI with JSON pointer", + "description": "unevaluatedItems.json: unevaluatedItems can see annotations from if without then and else", "schema": { - "$comment": "URIs do not have to have HTTP(s) schemes", "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:uuid:deadbeef-1234-00ff-ff00-4321feebdaed", - "properties": { - "foo": { - "$ref": "#/$defs/bar" - } + "if": { + "prefixItems": [ + { + "const": "a" + } + ] }, - "$defs": { - "bar": { - "type": "string" - } - } + "unevaluatedItems": false }, "tests": [ { - "description": "a string is valid", - "data": { - "foo": "bar" - }, + "description": "valid in case if is evaluated", + "data": [ + "a" + ], "valid": true }, { - "description": "a non-string is invalid", - "data": { - "foo": 12 - }, + "description": "invalid in case if is evaluated", + "data": [ + "b" + ], "valid": false } ] }, { - "description": "ref.json: URN base URI with NSS", + "description": "unevaluatedItems.json: Evaluated items collection needs to consider instance location", "schema": { - "$comment": "RFC 8141 §2.2", "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:example:1/406/47452/2", - "properties": { - "foo": { - "$ref": "#/$defs/bar" - } - }, - "$defs": { - "bar": { - "type": "string" + "prefixItems": [ + { + "prefixItems": [ + true, + { + "type": "string" + } + ] } - } + ], + "unevaluatedItems": false }, "tests": [ { - "description": "a string is valid", - "data": { - "foo": "bar" - }, - "valid": true - }, - { - "description": "a non-string is invalid", - "data": { - "foo": 12 - }, + "description": "with an unevaluated item that exists at another location", + "data": [ + [ + "foo", + "bar" + ], + "bar" + ], "valid": false } ] }, { - "description": "ref.json: URN base URI with r-component", + "description": "unevaluatedProperties.json: unevaluatedProperties true", "schema": { - "$comment": "RFC 8141 §2.3.1", "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:example:foo-bar-baz-qux?+CCResolve:cc=uk", - "properties": { - "foo": { - "$ref": "#/$defs/bar" - } - }, - "$defs": { - "bar": { - "type": "string" - } - } + "unevaluatedProperties": true }, "tests": [ { - "description": "a string is valid", - "data": { - "foo": "bar" - }, + "description": "with no unevaluated properties", + "data": {}, "valid": true }, { - "description": "a non-string is invalid", + "description": "with unevaluated properties", "data": { - "foo": 12 + "foo": "foo" }, - "valid": false + "valid": true } ] }, { - "description": "ref.json: URN base URI with q-component", + "description": "unevaluatedProperties.json: unevaluatedProperties schema", "schema": { - "$comment": "RFC 8141 §2.3.2", "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:example:weather?=op=map&lat=39.56&lon=-104.85&datetime=1969-07-21T02:56:15Z", - "properties": { - "foo": { - "$ref": "#/$defs/bar" - } - }, - "$defs": { - "bar": { - "type": "string" - } + "unevaluatedProperties": { + "type": "string", + "minLength": 3 } }, "tests": [ { - "description": "a string is valid", + "description": "with no unevaluated properties", + "data": {}, + "valid": true + }, + { + "description": "with valid unevaluated properties", "data": { - "foo": "bar" + "foo": "foo" }, "valid": true }, { - "description": "a non-string is invalid", + "description": "with invalid unevaluated properties", "data": { - "foo": 12 + "foo": "fo" }, "valid": false } ] }, { - "description": "ref.json: URN base URI with URN and JSON pointer ref", + "description": "unevaluatedProperties.json: unevaluatedProperties false", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:uuid:deadbeef-1234-0000-0000-4321feebdaed", - "properties": { - "foo": { - "$ref": "urn:uuid:deadbeef-1234-0000-0000-4321feebdaed#/$defs/bar" - } - }, - "$defs": { - "bar": { - "type": "string" - } - } + "unevaluatedProperties": false }, "tests": [ { - "description": "a string is valid", - "data": { - "foo": "bar" - }, + "description": "with no unevaluated properties", + "data": {}, "valid": true }, { - "description": "a non-string is invalid", + "description": "with unevaluated properties", "data": { - "foo": 12 + "foo": "foo" }, "valid": false } ] }, { - "description": "ref.json: URN base URI with URN and anchor ref", + "description": "unevaluatedProperties.json: unevaluatedProperties with adjacent properties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "urn:uuid:deadbeef-1234-ff00-00ff-4321feebdaed", "properties": { "foo": { - "$ref": "urn:uuid:deadbeef-1234-ff00-00ff-4321feebdaed#something" - } - }, - "$defs": { - "bar": { - "$anchor": "something", "type": "string" } - } + }, + "unevaluatedProperties": false }, "tests": [ { - "description": "a string is valid", + "description": "with no unevaluated properties", "data": { - "foo": "bar" + "foo": "foo" }, "valid": true }, { - "description": "a non-string is invalid", + "description": "with unevaluated properties", "data": { - "foo": 12 + "foo": "foo", + "bar": "bar" }, "valid": false } ] }, { - "description": "ref.json: URN ref with nested pointer ref", + "description": "unevaluatedProperties.json: unevaluatedProperties with adjacent patternProperties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", - "$defs": { - "foo": { - "$id": "urn:uuid:deadbeef-4321-ffff-ffff-1234feebdaed", - "$defs": { - "bar": { - "type": "string" - } - }, - "$ref": "#/$defs/bar" + "patternProperties": { + "^foo": { + "type": "string" } - } - }, - "tests": [ - { - "description": "a string is valid", - "data": "bar", - "valid": true }, - { - "description": "a non-string is invalid", - "data": 12, - "valid": false - } - ] - }, - { - "description": "ref.json: ref to if", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://example.com/ref/if", - "if": { - "$id": "http://example.com/ref/if", - "type": "integer" - } + "unevaluatedProperties": false }, "tests": [ { - "description": "a non-integer is invalid due to the $ref", - "data": "foo", - "valid": false - }, - { - "description": "an integer is valid", - "data": 12, + "description": "with no unevaluated properties", + "data": { + "foo": "foo" + }, "valid": true - } - ] - }, - { - "description": "ref.json: ref to then", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://example.com/ref/then", - "then": { - "$id": "http://example.com/ref/then", - "type": "integer" - } - }, - "tests": [ - { - "description": "a non-integer is invalid due to the $ref", - "data": "foo", - "valid": false }, { - "description": "an integer is valid", - "data": 12, - "valid": true - } - ] - }, - { - "description": "ref.json: ref to else", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://example.com/ref/else", - "else": { - "$id": "http://example.com/ref/else", - "type": "integer" - } - }, - "tests": [ - { - "description": "a non-integer is invalid due to the $ref", - "data": "foo", + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, "valid": false - }, - { - "description": "an integer is valid", - "data": 12, - "valid": true } ] }, { - "description": "ref.json: ref with absolute-path-reference", + "description": "unevaluatedProperties.json: unevaluatedProperties with adjacent bool additionalProperties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://example.com/ref/absref.json", - "$defs": { - "a": { - "$id": "http://example.com/ref/absref/foobar.json", - "type": "number" - }, - "b": { - "$id": "http://example.com/absref/foobar.json", + "type": "object", + "properties": { + "foo": { "type": "string" } }, - "$ref": "/absref/foobar.json" + "additionalProperties": true, + "unevaluatedProperties": false }, "tests": [ { - "description": "a string is valid", - "data": "foo", + "description": "with no additional properties", + "data": { + "foo": "foo" + }, "valid": true }, { - "description": "an integer is invalid", - "data": 12, - "valid": false + "description": "with additional properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true } ] }, { - "description": "ref.json: $id with file URI still resolves pointers - *nix", + "description": "unevaluatedProperties.json: unevaluatedProperties with adjacent non-bool additionalProperties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "file:///folder/file.json", - "$defs": { + "type": "object", + "properties": { "foo": { - "type": "number" + "type": "string" } }, - "$ref": "#/$defs/foo" + "additionalProperties": { + "type": "string" + }, + "unevaluatedProperties": false }, "tests": [ { - "description": "number is valid", - "data": 1, + "description": "with no additional properties", + "data": { + "foo": "foo" + }, "valid": true }, { - "description": "non-number is invalid", - "data": "a", - "valid": false + "description": "with additional properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true } ] }, { - "description": "ref.json: $id with file URI still resolves pointers - windows", + "description": "unevaluatedProperties.json: unevaluatedProperties with nested properties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "file:///c:/folder/file.json", - "$defs": { + "properties": { "foo": { - "type": "number" + "type": "string" } }, - "$ref": "#/$defs/foo" + "allOf": [ + { + "properties": { + "bar": { + "type": "string" + } + } + } + ], + "unevaluatedProperties": false }, "tests": [ { - "description": "number is valid", - "data": 1, + "description": "with no additional properties", + "data": { + "foo": "foo", + "bar": "bar" + }, "valid": true }, { - "description": "non-number is invalid", - "data": "a", + "description": "with additional properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, "valid": false } ] }, { - "description": "ref.json: empty tokens in $ref json-pointer", + "description": "unevaluatedProperties.json: unevaluatedProperties with nested patternProperties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "": { - "$defs": { - "": { - "type": "number" - } - } + "properties": { + "foo": { + "type": "string" } }, "allOf": [ { - "$ref": "#/$defs//$defs/" + "patternProperties": { + "^bar": { + "type": "string" + } + } } - ] + ], + "unevaluatedProperties": false }, "tests": [ { - "description": "number is valid", - "data": 1, + "description": "with no additional properties", + "data": { + "foo": "foo", + "bar": "bar" + }, "valid": true }, { - "description": "non-number is invalid", - "data": "a", + "description": "with additional properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, "valid": false } ] }, { - "description": "refRemote.json: remote ref", + "description": "unevaluatedProperties.json: unevaluatedProperties with nested additionalProperties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/integer.json" - }, - "documents": { - "http://localhost:1234/draft2020-12/integer.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "integer" - } + "properties": { + "foo": { + "type": "string" + } + }, + "allOf": [ + { + "additionalProperties": true + } + ], + "unevaluatedProperties": false }, "tests": [ { - "description": "remote ref valid", - "data": 1, + "description": "with no additional properties", + "data": { + "foo": "foo" + }, "valid": true }, { - "description": "remote ref invalid", - "data": "a", - "valid": false + "description": "with additional properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true } ] }, { - "description": "refRemote.json: fragment within remote ref", + "description": "unevaluatedProperties.json: unevaluatedProperties with nested unevaluatedProperties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/integer" - }, - "documents": { - "http://localhost:1234/draft2020-12/subSchemas.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/$defs/integer" - } + "properties": { + "foo": { + "type": "string" + } + }, + "allOf": [ + { + "unevaluatedProperties": true } + ], + "unevaluatedProperties": { + "type": "string", + "maxLength": 2 } }, "tests": [ { - "description": "remote fragment valid", - "data": 1, + "description": "with no nested unevaluated properties", + "data": { + "foo": "foo" + }, "valid": true }, { - "description": "remote fragment invalid", - "data": "a", - "valid": false + "description": "with nested unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true } ] }, { - "description": "refRemote.json: anchor within remote ref", + "description": "unevaluatedProperties.json: unevaluatedProperties with anyOf", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/locationIndependentIdentifier.json#foo" - }, - "documents": { - "http://localhost:1234/draft2020-12/locationIndependentIdentifier.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "refToInteger": { - "$ref": "#foo" + "properties": { + "foo": { + "type": "string" + } + }, + "anyOf": [ + { + "properties": { + "bar": { + "const": "bar" + } }, - "A": { - "$anchor": "foo", - "type": "integer" - } + "required": [ + "bar" + ] + }, + { + "properties": { + "baz": { + "const": "baz" + } + }, + "required": [ + "baz" + ] + }, + { + "properties": { + "quux": { + "const": "quux" + } + }, + "required": [ + "quux" + ] } - } + ], + "unevaluatedProperties": false }, "tests": [ { - "description": "remote anchor valid", - "data": 1, + "description": "when one matches and has no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, "valid": true }, { - "description": "remote anchor invalid", - "data": "a", + "description": "when one matches and has unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "not-baz" + }, "valid": false - } - ] - }, - { - "description": "refRemote.json: ref within remote ref", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/subSchemas.json#/$defs/refToInteger" - }, - "documents": { - "http://localhost:1234/draft2020-12/subSchemas.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "integer": { - "type": "integer" - }, - "refToInteger": { - "$ref": "#/$defs/integer" - } - } - } - }, - "tests": [ + }, { - "description": "ref within ref valid", - "data": 1, + "description": "when two match and has no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, "valid": true }, { - "description": "ref within ref invalid", - "data": "a", + "description": "when two match and has unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz", + "quux": "not-quux" + }, "valid": false } ] }, { - "description": "refRemote.json: base URI change", + "description": "unevaluatedProperties.json: unevaluatedProperties with oneOf", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/", - "items": { - "$id": "baseUriChange/", - "items": { - "$ref": "folderInteger.json" + "properties": { + "foo": { + "type": "string" } - } - }, - "documents": { - "http://localhost:1234/draft2020-12/baseUriChange/folderInteger.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "integer" - } + }, + "oneOf": [ + { + "properties": { + "bar": { + "const": "bar" + } + }, + "required": [ + "bar" + ] + }, + { + "properties": { + "baz": { + "const": "baz" + } + }, + "required": [ + "baz" + ] + } + ], + "unevaluatedProperties": false }, "tests": [ { - "description": "base URI change ref valid", - "data": [ - [ - 1 - ] - ], + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, "valid": true }, { - "description": "base URI change ref invalid", - "data": [ - [ - "a" - ] - ], + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "quux": "quux" + }, "valid": false } ] }, { - "description": "refRemote.json: base URI change - change folder", + "description": "unevaluatedProperties.json: unevaluatedProperties with not", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/scope_change_defs1.json", - "type": "object", "properties": { - "list": { - "$ref": "baseUriChangeFolder/" + "foo": { + "type": "string" } }, - "$defs": { - "baz": { - "$id": "baseUriChangeFolder/", - "type": "array", - "items": { - "$ref": "folderInteger.json" - } - } - } - }, - "documents": { - "http://localhost:1234/draft2020-12/baseUriChangeFolder/folderInteger.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "integer" - } - }, - "tests": [ - { - "description": "number is valid", - "data": { - "list": [ - 1 + "not": { + "not": { + "properties": { + "bar": { + "const": "bar" + } + }, + "required": [ + "bar" ] - }, - "valid": true + } }, + "unevaluatedProperties": false + }, + "tests": [ { - "description": "string is invalid", + "description": "with unevaluated properties", "data": { - "list": [ - "a" - ] + "foo": "foo", + "bar": "bar" }, "valid": false } ] }, { - "description": "refRemote.json: base URI change - change folder in subschema", + "description": "unevaluatedProperties.json: unevaluatedProperties with if/then/else", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/scope_change_defs2.json", - "type": "object", - "properties": { - "list": { - "$ref": "baseUriChangeFolderInSubschema/#/$defs/bar" - } + "if": { + "properties": { + "foo": { + "const": "then" + } + }, + "required": [ + "foo" + ] }, - "$defs": { - "baz": { - "$id": "baseUriChangeFolderInSubschema/", - "$defs": { - "bar": { - "type": "array", - "items": { - "$ref": "folderInteger.json" - } - } + "then": { + "properties": { + "bar": { + "type": "string" } - } - } - }, - "documents": { - "http://localhost:1234/draft2020-12/baseUriChangeFolderInSubschema/folderInteger.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "integer" - } + }, + "required": [ + "bar" + ] + }, + "else": { + "properties": { + "baz": { + "type": "string" + } + }, + "required": [ + "baz" + ] + }, + "unevaluatedProperties": false }, "tests": [ { - "description": "number is valid", + "description": "when if is true and has no unevaluated properties", "data": { - "list": [ - 1 - ] + "foo": "then", + "bar": "bar" }, "valid": true }, { - "description": "string is invalid", + "description": "when if is true and has unevaluated properties", "data": { - "list": [ - "a" - ] + "foo": "then", + "bar": "bar", + "baz": "baz" + }, + "valid": false + }, + { + "description": "when if is false and has no unevaluated properties", + "data": { + "baz": "baz" + }, + "valid": true + }, + { + "description": "when if is false and has unevaluated properties", + "data": { + "foo": "else", + "baz": "baz" }, "valid": false } ] }, { - "description": "refRemote.json: root ref in remote ref", + "description": "unevaluatedProperties.json: unevaluatedProperties with if/then/else, then not defined", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/object", - "type": "object", - "properties": { - "name": { - "$ref": "name-defs.json#/$defs/orNull" - } - } - }, - "documents": { - "http://localhost:1234/draft2020-12/name-defs.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "orNull": { - "anyOf": [ - { - "type": "null" - }, - { - "$ref": "#" - } - ] + "if": { + "properties": { + "foo": { + "const": "then" } }, - "type": "string" - } + "required": [ + "foo" + ] + }, + "else": { + "properties": { + "baz": { + "type": "string" + } + }, + "required": [ + "baz" + ] + }, + "unevaluatedProperties": false }, "tests": [ { - "description": "string is valid", + "description": "when if is true and has no unevaluated properties", "data": { - "name": "foo" + "foo": "then" }, "valid": true }, { - "description": "null is valid", + "description": "when if is true and has unevaluated properties", "data": { - "name": null + "foo": "then", + "bar": "bar" + }, + "valid": false + }, + { + "description": "when if is false and has no unevaluated properties", + "data": { + "baz": "baz" }, "valid": true }, { - "description": "object is invalid", + "description": "when if is false and has unevaluated properties", "data": { - "name": { - "name": null - } + "foo": "else", + "baz": "baz" }, "valid": false } ] }, { - "description": "refRemote.json: remote ref with ref to defs", + "description": "unevaluatedProperties.json: unevaluatedProperties with if/then/else, else not defined", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/schema-remote-ref-ref-defs1.json", - "$ref": "ref-and-defs.json" - }, - "documents": { - "http://localhost:1234/draft2020-12/ref-and-defs.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/ref-and-defs.json", - "$defs": { - "inner": { - "properties": { - "bar": { - "type": "string" - } - } + "if": { + "properties": { + "foo": { + "const": "then" } }, - "$ref": "#/$defs/inner" - } + "required": [ + "foo" + ] + }, + "then": { + "properties": { + "bar": { + "type": "string" + } + }, + "required": [ + "bar" + ] + }, + "unevaluatedProperties": false }, "tests": [ { - "description": "invalid", + "description": "when if is true and has no unevaluated properties", "data": { - "bar": 1 + "foo": "then", + "bar": "bar" + }, + "valid": true + }, + { + "description": "when if is true and has unevaluated properties", + "data": { + "foo": "then", + "bar": "bar", + "baz": "baz" }, "valid": false }, { - "description": "valid", + "description": "when if is false and has no unevaluated properties", "data": { - "bar": "a" + "baz": "baz" }, - "valid": true + "valid": false + }, + { + "description": "when if is false and has unevaluated properties", + "data": { + "foo": "else", + "baz": "baz" + }, + "valid": false } ] }, { - "description": "refRemote.json: Location-independent identifier in remote ref", + "description": "unevaluatedProperties.json: unevaluatedProperties with dependentSchemas", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/locationIndependentIdentifier.json#/$defs/refToInteger" - }, - "documents": { - "http://localhost:1234/draft2020-12/locationIndependentIdentifier.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "refToInteger": { - "$ref": "#foo" + "properties": { + "foo": { + "type": "string" + } + }, + "dependentSchemas": { + "foo": { + "properties": { + "bar": { + "const": "bar" + } }, - "A": { - "$anchor": "foo", - "type": "integer" - } + "required": [ + "bar" + ] } - } + }, + "unevaluatedProperties": false }, "tests": [ { - "description": "integer is valid", - "data": 1, + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, "valid": true }, { - "description": "string is invalid", - "data": "foo", + "description": "with unevaluated properties", + "data": { + "bar": "bar" + }, "valid": false } ] }, { - "description": "refRemote.json: retrieved nested refs resolve relative to their URI not $id", + "description": "unevaluatedProperties.json: unevaluatedProperties with boolean schemas", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "http://localhost:1234/draft2020-12/some-id", "properties": { - "name": { - "$ref": "nested/foo-ref-string.json" - } - } - }, - "documents": { - "http://localhost:1234/draft2020-12/nested/foo-ref-string.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "foo": { - "$ref": "string.json" - } + "foo": { + "type": "string" } }, - "http://localhost:1234/draft2020-12/nested/string.json": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "string" - } + "allOf": [ + true + ], + "unevaluatedProperties": false }, "tests": [ { - "description": "number is invalid", + "description": "with no unevaluated properties", "data": { - "name": { - "foo": 1 - } + "foo": "foo" }, - "valid": false + "valid": true }, { - "description": "string is valid", + "description": "with unevaluated properties", "data": { - "name": { - "foo": "a" - } + "bar": "bar" }, - "valid": true + "valid": false } ] }, { - "description": "refRemote.json: remote HTTP ref with different $id", + "description": "unevaluatedProperties.json: unevaluatedProperties with $ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/different-id-ref-string.json" - }, - "documents": { - "http://localhost:1234/draft2020-12/different-id-ref-string.json": { - "$id": "http://localhost:1234/draft2020-12/real-id-ref-string.json", - "$defs": { - "bar": { - "type": "string" + "$ref": "#/$defs/bar", + "properties": { + "foo": { + "type": "string" + } + }, + "unevaluatedProperties": false, + "$defs": { + "bar": { + "properties": { + "bar": { + "type": "string" + } } - }, - "$ref": "#/$defs/bar" + } } }, "tests": [ { - "description": "number is invalid", - "data": 1, - "valid": false + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true }, { - "description": "string is valid", - "data": "foo", - "valid": true + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false } ] }, { - "description": "refRemote.json: remote HTTP ref with different URN $id", + "description": "unevaluatedProperties.json: unevaluatedProperties before $ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/urn-ref-string.json" - }, - "documents": { - "http://localhost:1234/draft2020-12/urn-ref-string.json": { - "$id": "urn:uuid:feebdaed-ffff-0000-2020-1200deadbeef", - "$defs": { - "bar": { - "type": "string" + "unevaluatedProperties": false, + "properties": { + "foo": { + "type": "string" + } + }, + "$ref": "#/$defs/bar", + "$defs": { + "bar": { + "properties": { + "bar": { + "type": "string" + } } - }, - "$ref": "#/$defs/bar" + } } }, "tests": [ { - "description": "number is invalid", - "data": 1, - "valid": false + "description": "with no unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": true }, { - "description": "string is valid", - "data": "foo", - "valid": true + "description": "with unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar", + "baz": "baz" + }, + "valid": false } ] }, { - "description": "refRemote.json: remote HTTP ref with nested absolute ref", + "description": "unevaluatedProperties.json: unevaluatedProperties can't see inside cousins", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/nested-absolute-ref-to-string.json" - }, - "documents": { - "http://localhost:1234/draft2020-12/nested-absolute-ref-to-string.json": { - "$defs": { - "bar": { - "$id": "http://localhost:1234/draft2020-12/the-nested-id.json", - "type": "string" + "allOf": [ + { + "properties": { + "foo": true } }, - "$ref": "http://localhost:1234/draft2020-12/the-nested-id.json" - } + { + "unevaluatedProperties": false + } + ] }, "tests": [ { - "description": "number is invalid", - "data": 1, + "description": "always fails", + "data": { + "foo": 1 + }, "valid": false - }, - { - "description": "string is valid", - "data": "foo", - "valid": true } ] }, { - "description": "refRemote.json: $ref to $ref finds detached $anchor", + "description": "unevaluatedProperties.json: unevaluatedProperties can't see inside cousins (reverse order)", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$ref": "http://localhost:1234/draft2020-12/detached-ref.json#/$defs/foo" - }, - "documents": { - "http://localhost:1234/draft2020-12/detached-ref.json": { - "$id": "http://localhost:1234/draft2020-12/detached-ref.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$defs": { - "foo": { - "$ref": "#detached" - }, - "detached": { - "$anchor": "detached", - "type": "integer" + "allOf": [ + { + "unevaluatedProperties": false + }, + { + "properties": { + "foo": true } } - } + ] }, "tests": [ { - "description": "number is valid", - "data": 1, - "valid": true - }, - { - "description": "non-number is invalid", - "data": "a", + "description": "always fails", + "data": { + "foo": 1 + }, "valid": false } ] }, { - "description": "required.json: required validation", + "description": "unevaluatedProperties.json: nested unevaluatedProperties, outer false, inner true, properties outside", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { - "foo": {}, - "bar": {} + "foo": { + "type": "string" + } }, - "required": [ - "foo" - ] + "allOf": [ + { + "unevaluatedProperties": true + } + ], + "unevaluatedProperties": false }, "tests": [ { - "description": "present required property is valid", + "description": "with no nested unevaluated properties", "data": { - "foo": 1 + "foo": "foo" }, "valid": true }, { - "description": "non-present required property is invalid", + "description": "with nested unevaluated properties", "data": { - "bar": 1 + "foo": "foo", + "bar": "bar" }, - "valid": false - }, - { - "description": "ignores arrays", - "data": [], - "valid": true - }, - { - "description": "ignores strings", - "data": "", - "valid": true - }, - { - "description": "ignores other non-objects", - "data": 12, - "valid": true - }, - { - "description": "ignores null", - "data": null, - "valid": true - }, - { - "description": "ignores boolean", - "data": true, "valid": true } ] }, { - "description": "required.json: required default validation", + "description": "unevaluatedProperties.json: nested unevaluatedProperties, outer false, inner true, properties inside", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "foo": {} - } + "allOf": [ + { + "properties": { + "foo": { + "type": "string" + } + }, + "unevaluatedProperties": true + } + ], + "unevaluatedProperties": false }, "tests": [ { - "description": "not required by default", - "data": {}, + "description": "with no nested unevaluated properties", + "data": { + "foo": "foo" + }, + "valid": true + }, + { + "description": "with nested unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, "valid": true } ] }, { - "description": "required.json: required with empty array", + "description": "unevaluatedProperties.json: nested unevaluatedProperties, outer true, inner false, properties outside", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { - "foo": {} + "foo": { + "type": "string" + } }, - "required": [] + "allOf": [ + { + "unevaluatedProperties": false + } + ], + "unevaluatedProperties": true }, "tests": [ { - "description": "property not required", - "data": {}, - "valid": true + "description": "with no nested unevaluated properties", + "data": { + "foo": "foo" + }, + "valid": false + }, + { + "description": "with nested unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, + "valid": false } ] }, { - "description": "required.json: required with escaped characters", + "description": "unevaluatedProperties.json: nested unevaluatedProperties, outer true, inner false, properties inside", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "required": [ - "foo\nbar", - "foo\"bar", - "foo\\bar", - "foo\rbar", - "foo\tbar", - "foo\fbar" - ] + "allOf": [ + { + "properties": { + "foo": { + "type": "string" + } + }, + "unevaluatedProperties": false + } + ], + "unevaluatedProperties": true }, "tests": [ { - "description": "object with all properties present is valid", + "description": "with no nested unevaluated properties", "data": { - "foo\nbar": 1, - "foo\"bar": 1, - "foo\\bar": 1, - "foo\rbar": 1, - "foo\tbar": 1, - "foo\fbar": 1 + "foo": "foo" }, "valid": true }, { - "description": "object with some properties missing is invalid", + "description": "with nested unevaluated properties", "data": { - "foo\nbar": "1", - "foo\"bar": "1" + "foo": "foo", + "bar": "bar" }, "valid": false } ] }, { - "description": "required.json: required properties whose names are Javascript object property names", + "description": "unevaluatedProperties.json: cousin unevaluatedProperties, true and false, true with properties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "required": [ - "__proto__", - "toString", - "constructor" + "allOf": [ + { + "properties": { + "foo": { + "type": "string" + } + }, + "unevaluatedProperties": true + }, + { + "unevaluatedProperties": false + } ] }, "tests": [ { - "description": "ignores arrays", - "data": [], - "valid": true - }, - { - "description": "ignores other non-objects", - "data": 12, - "valid": true - }, - { - "description": "none of the properties mentioned", - "data": {}, - "valid": false - }, - { - "description": "__proto__ present", - "data": { - "__proto__": "foo" - }, - "valid": false - }, - { - "description": "toString present", + "description": "with no nested unevaluated properties", "data": { - "toString": { - "length": 37 - } + "foo": "foo" }, "valid": false }, { - "description": "constructor present", + "description": "with nested unevaluated properties", "data": { - "constructor": { - "length": 37 - } + "foo": "foo", + "bar": "bar" }, "valid": false - }, - { - "description": "all present", - "data": { - "__proto__": 12, - "toString": { - "length": "foo" - }, - "constructor": 37 - }, - "valid": true } ] }, { - "description": "type.json: integer type matches integers", + "description": "unevaluatedProperties.json: cousin unevaluatedProperties, true and false, false with properties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "integer" + "allOf": [ + { + "unevaluatedProperties": true + }, + { + "properties": { + "foo": { + "type": "string" + } + }, + "unevaluatedProperties": false + } + ] }, "tests": [ { - "description": "an integer is an integer", - "data": 1, - "valid": true - }, - { - "description": "a float with zero fractional part is an integer", - "data": 1.0, + "description": "with no nested unevaluated properties", + "data": { + "foo": "foo" + }, "valid": true }, { - "description": "a float is not an integer", - "data": 1.1, - "valid": false - }, - { - "description": "a string is not an integer", - "data": "foo", - "valid": false - }, - { - "description": "a string is still not an integer, even if it looks like one", - "data": "1", - "valid": false - }, - { - "description": "an object is not an integer", - "data": {}, - "valid": false - }, - { - "description": "an array is not an integer", - "data": [], - "valid": false - }, - { - "description": "a boolean is not an integer", - "data": true, - "valid": false - }, - { - "description": "null is not an integer", - "data": null, + "description": "with nested unevaluated properties", + "data": { + "foo": "foo", + "bar": "bar" + }, "valid": false } ] }, { - "description": "type.json: number type matches numbers", + "description": "unevaluatedProperties.json: property is evaluated in an uncle schema to unevaluatedProperties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "number" + "properties": { + "foo": { + "properties": { + "bar": { + "type": "string" + } + }, + "unevaluatedProperties": false + } + }, + "anyOf": [ + { + "properties": { + "foo": { + "properties": { + "faz": { + "type": "string" + } + } + } + } + } + ] }, "tests": [ { - "description": "an integer is a number", - "data": 1, - "valid": true - }, - { - "description": "a float with zero fractional part is a number (and an integer)", - "data": 1.0, - "valid": true - }, - { - "description": "a float is a number", - "data": 1.1, + "description": "no extra properties", + "data": { + "foo": { + "bar": "test" + } + }, "valid": true }, { - "description": "a string is not a number", - "data": "foo", - "valid": false - }, - { - "description": "a string is still not a number, even if it looks like one", - "data": "1", - "valid": false - }, - { - "description": "an object is not a number", - "data": {}, - "valid": false - }, - { - "description": "an array is not a number", - "data": [], - "valid": false - }, - { - "description": "a boolean is not a number", - "data": true, - "valid": false - }, - { - "description": "null is not a number", - "data": null, + "description": "uncle keyword evaluation is not significant", + "data": { + "foo": { + "bar": "test", + "faz": "test" + } + }, "valid": false } ] }, { - "description": "type.json: string type matches strings", + "description": "unevaluatedProperties.json: in-place applicator siblings, allOf has unevaluated", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "string" + "allOf": [ + { + "properties": { + "foo": true + }, + "unevaluatedProperties": false + } + ], + "anyOf": [ + { + "properties": { + "bar": true + } + } + ] }, "tests": [ { - "description": "1 is not a string", - "data": 1, - "valid": false - }, - { - "description": "a float is not a string", - "data": 1.1, + "description": "base case: both properties present", + "data": { + "foo": 1, + "bar": 1 + }, "valid": false }, { - "description": "a string is a string", - "data": "foo", - "valid": true - }, - { - "description": "a string is still a string, even if it looks like a number", - "data": "1", - "valid": true - }, - { - "description": "an empty string is still a string", - "data": "", + "description": "in place applicator siblings, bar is missing", + "data": { + "foo": 1 + }, "valid": true }, { - "description": "an object is not a string", - "data": {}, + "description": "in place applicator siblings, foo is missing", + "data": { + "bar": 1 + }, "valid": false - }, + } + ] + }, + { + "description": "unevaluatedProperties.json: in-place applicator siblings, anyOf has unevaluated", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "allOf": [ + { + "properties": { + "foo": true + } + } + ], + "anyOf": [ + { + "properties": { + "bar": true + }, + "unevaluatedProperties": false + } + ] + }, + "tests": [ { - "description": "an array is not a string", - "data": [], + "description": "base case: both properties present", + "data": { + "foo": 1, + "bar": 1 + }, "valid": false }, { - "description": "a boolean is not a string", - "data": true, + "description": "in place applicator siblings, bar is missing", + "data": { + "foo": 1 + }, "valid": false }, { - "description": "null is not a string", - "data": null, - "valid": false + "description": "in place applicator siblings, foo is missing", + "data": { + "bar": 1 + }, + "valid": true } ] }, { - "description": "type.json: object type matches objects", + "description": "unevaluatedProperties.json: unevaluatedProperties + single cyclic ref", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object" + "properties": { + "x": { + "$ref": "#" + } + }, + "unevaluatedProperties": false }, "tests": [ { - "description": "an integer is not an object", - "data": 1, - "valid": false + "description": "Empty is valid", + "data": {}, + "valid": true }, { - "description": "a float is not an object", - "data": 1.1, - "valid": false + "description": "Single is valid", + "data": { + "x": {} + }, + "valid": true }, { - "description": "a string is not an object", - "data": "foo", + "description": "Unevaluated on 1st level is invalid", + "data": { + "x": {}, + "y": {} + }, "valid": false }, { - "description": "an object is an object", - "data": {}, + "description": "Nested is valid", + "data": { + "x": { + "x": {} + } + }, "valid": true }, { - "description": "an array is not an object", - "data": [], + "description": "Unevaluated on 2nd level is invalid", + "data": { + "x": { + "x": {}, + "y": {} + } + }, "valid": false }, { - "description": "a boolean is not an object", - "data": true, - "valid": false + "description": "Deep nested is valid", + "data": { + "x": { + "x": { + "x": {} + } + } + }, + "valid": true }, { - "description": "null is not an object", - "data": null, + "description": "Unevaluated on 3rd level is invalid", + "data": { + "x": { + "x": { + "x": {}, + "y": {} + } + } + }, "valid": false } ] }, { - "description": "type.json: array type matches arrays", + "description": "unevaluatedProperties.json: unevaluatedProperties + ref inside allOf / oneOf", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "array" + "$defs": { + "one": { + "properties": { + "a": true + } + }, + "two": { + "required": [ + "x" + ], + "properties": { + "x": true + } + } + }, + "allOf": [ + { + "$ref": "#/$defs/one" + }, + { + "properties": { + "b": true + } + }, + { + "oneOf": [ + { + "$ref": "#/$defs/two" + }, + { + "required": [ + "y" + ], + "properties": { + "y": true + } + } + ] + } + ], + "unevaluatedProperties": false }, "tests": [ { - "description": "an integer is not an array", - "data": 1, + "description": "Empty is invalid (no x or y)", + "data": {}, "valid": false }, { - "description": "a float is not an array", - "data": 1.1, + "description": "a and b are invalid (no x or y)", + "data": { + "a": 1, + "b": 1 + }, "valid": false }, { - "description": "a string is not an array", - "data": "foo", + "description": "x and y are invalid", + "data": { + "x": 1, + "y": 1 + }, "valid": false }, { - "description": "an object is not an array", - "data": {}, - "valid": false + "description": "a and x are valid", + "data": { + "a": 1, + "x": 1 + }, + "valid": true }, { - "description": "an array is an array", - "data": [], + "description": "a and y are valid", + "data": { + "a": 1, + "y": 1 + }, "valid": true }, { - "description": "a boolean is not an array", - "data": true, - "valid": false + "description": "a and b and x are valid", + "data": { + "a": 1, + "b": 1, + "x": 1 + }, + "valid": true }, { - "description": "null is not an array", - "data": null, + "description": "a and b and y are valid", + "data": { + "a": 1, + "b": 1, + "y": 1 + }, + "valid": true + }, + { + "description": "a and b and x and y are invalid", + "data": { + "a": 1, + "b": 1, + "x": 1, + "y": 1 + }, "valid": false } ] }, { - "description": "type.json: boolean type matches booleans", + "description": "unevaluatedProperties.json: dynamic evalation inside nested refs", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "boolean" + "$defs": { + "one": { + "oneOf": [ + { + "$ref": "#/$defs/two" + }, + { + "required": [ + "b" + ], + "properties": { + "b": true + } + }, + { + "required": [ + "xx" + ], + "patternProperties": { + "x": true + } + }, + { + "required": [ + "all" + ], + "unevaluatedProperties": true + } + ] + }, + "two": { + "oneOf": [ + { + "required": [ + "c" + ], + "properties": { + "c": true + } + }, + { + "required": [ + "d" + ], + "properties": { + "d": true + } + } + ] + } + }, + "oneOf": [ + { + "$ref": "#/$defs/one" + }, + { + "required": [ + "a" + ], + "properties": { + "a": true + } + } + ], + "unevaluatedProperties": false }, "tests": [ { - "description": "an integer is not a boolean", - "data": 1, + "description": "Empty is invalid", + "data": {}, "valid": false }, { - "description": "zero is not a boolean", - "data": 0, - "valid": false + "description": "a is valid", + "data": { + "a": 1 + }, + "valid": true }, { - "description": "a float is not a boolean", - "data": 1.1, - "valid": false + "description": "b is valid", + "data": { + "b": 1 + }, + "valid": true }, { - "description": "a string is not a boolean", - "data": "foo", - "valid": false + "description": "c is valid", + "data": { + "c": 1 + }, + "valid": true }, { - "description": "an empty string is not a boolean", - "data": "", - "valid": false + "description": "d is valid", + "data": { + "d": 1 + }, + "valid": true }, { - "description": "an object is not a boolean", - "data": {}, + "description": "a + b is invalid", + "data": { + "a": 1, + "b": 1 + }, "valid": false }, { - "description": "an array is not a boolean", - "data": [], + "description": "a + c is invalid", + "data": { + "a": 1, + "c": 1 + }, "valid": false }, { - "description": "true is a boolean", - "data": true, - "valid": true + "description": "a + d is invalid", + "data": { + "a": 1, + "d": 1 + }, + "valid": false }, { - "description": "false is a boolean", - "data": false, - "valid": true + "description": "b + c is invalid", + "data": { + "b": 1, + "c": 1 + }, + "valid": false }, { - "description": "null is not a boolean", - "data": null, + "description": "b + d is invalid", + "data": { + "b": 1, + "d": 1 + }, "valid": false - } - ] - }, - { - "description": "type.json: null type matches only the null object", - "schema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "null" - }, - "tests": [ + }, { - "description": "an integer is not null", - "data": 1, + "description": "c + d is invalid", + "data": { + "c": 1, + "d": 1 + }, "valid": false }, { - "description": "a float is not null", - "data": 1.1, - "valid": false + "description": "xx is valid", + "data": { + "xx": 1 + }, + "valid": true }, { - "description": "zero is not null", - "data": 0, - "valid": false + "description": "xx + foox is valid", + "data": { + "xx": 1, + "foox": 1 + }, + "valid": true }, { - "description": "a string is not null", - "data": "foo", + "description": "xx + foo is invalid", + "data": { + "xx": 1, + "foo": 1 + }, "valid": false }, { - "description": "an empty string is not null", - "data": "", + "description": "xx + a is invalid", + "data": { + "xx": 1, + "a": 1 + }, "valid": false }, { - "description": "an object is not null", - "data": {}, + "description": "xx + b is invalid", + "data": { + "xx": 1, + "b": 1 + }, "valid": false }, { - "description": "an array is not null", - "data": [], + "description": "xx + c is invalid", + "data": { + "xx": 1, + "c": 1 + }, "valid": false }, { - "description": "true is not null", - "data": true, + "description": "xx + d is invalid", + "data": { + "xx": 1, + "d": 1 + }, "valid": false }, { - "description": "false is not null", - "data": false, - "valid": false + "description": "all is valid", + "data": { + "all": 1 + }, + "valid": true }, { - "description": "null is null", - "data": null, + "description": "all + foo is valid", + "data": { + "all": 1, + "foo": 1 + }, "valid": true + }, + { + "description": "all + a is invalid", + "data": { + "all": 1, + "a": 1 + }, + "valid": false } ] }, { - "description": "type.json: multiple types can be specified in an array", + "description": "unevaluatedProperties.json: non-object instances are valid", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": [ - "integer", - "string" - ] + "unevaluatedProperties": false }, "tests": [ { - "description": "an integer is valid", - "data": 1, + "description": "ignores booleans", + "data": true, "valid": true }, { - "description": "a string is valid", - "data": "foo", + "description": "ignores integers", + "data": 123, "valid": true }, { - "description": "a float is invalid", - "data": 1.1, - "valid": false - }, - { - "description": "an object is invalid", - "data": {}, - "valid": false + "description": "ignores floats", + "data": 1.0, + "valid": true }, { - "description": "an array is invalid", + "description": "ignores arrays", "data": [], - "valid": false + "valid": true }, { - "description": "a boolean is invalid", - "data": true, - "valid": false + "description": "ignores strings", + "data": "foo", + "valid": true }, { - "description": "null is invalid", + "description": "ignores null", "data": null, - "valid": false + "valid": true } ] }, { - "description": "type.json: type as array with one item", + "description": "unevaluatedProperties.json: unevaluatedProperties with null valued instance properties", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": [ - "string" - ] + "unevaluatedProperties": { + "type": "null" + } }, "tests": [ { - "description": "string is valid", - "data": "foo", + "description": "allows null valued properties", + "data": { + "foo": null + }, + "valid": true + } + ] + }, + { + "description": "unevaluatedProperties.json: unevaluatedProperties not affected by propertyNames", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "propertyNames": { + "maxLength": 1 + }, + "unevaluatedProperties": { + "type": "number" + } + }, + "tests": [ + { + "description": "allows only number properties", + "data": { + "a": 1 + }, "valid": true }, { - "description": "number is invalid", - "data": 123, + "description": "string property is invalid", + "data": { + "a": "b" + }, "valid": false } ] }, { - "description": "type.json: type: array or object", + "description": "unevaluatedProperties.json: unevaluatedProperties can see annotations from if without then and else", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": [ - "array", - "object" - ] + "if": { + "patternProperties": { + "foo": { + "type": "string" + } + } + }, + "unevaluatedProperties": false }, "tests": [ { - "description": "array is valid", - "data": [ - 1, - 2, - 3 - ], + "description": "valid in case if is evaluated", + "data": { + "foo": "a" + }, "valid": true }, { - "description": "object is valid", + "description": "invalid in case if is evaluated", "data": { - "foo": 123 + "bar": "a" }, - "valid": true + "valid": false + } + ] + }, + { + "description": "unevaluatedProperties.json: dependentSchemas with unevaluatedProperties", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo2": {} + }, + "dependentSchemas": { + "foo": {}, + "foo2": { + "properties": { + "bar": {} + } + } }, + "unevaluatedProperties": false + }, + "tests": [ { - "description": "number is invalid", - "data": 123, + "description": "unevaluatedProperties doesn't consider dependentSchemas", + "data": { + "foo": "" + }, "valid": false }, { - "description": "string is invalid", - "data": "foo", + "description": "unevaluatedProperties doesn't see bar when foo2 is absent", + "data": { + "bar": "" + }, "valid": false }, { - "description": "null is invalid", - "data": null, - "valid": false + "description": "unevaluatedProperties sees bar when foo2 is present", + "data": { + "foo2": "", + "bar": "" + }, + "valid": true } ] }, { - "description": "type.json: type: array, object or null", + "description": "unevaluatedProperties.json: Evaluated properties collection needs to consider instance location", "schema": { "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": [ - "array", - "object", - "null" - ] + "properties": { + "foo": { + "properties": { + "bar": { + "type": "string" + } + } + } + }, + "unevaluatedProperties": false }, "tests": [ { - "description": "array is valid", - "data": [ - 1, - 2, - 3 - ], - "valid": true + "description": "with an unevaluated property that exists at another location", + "data": { + "foo": { + "bar": "foo" + }, + "bar": "bar" + }, + "valid": false + } + ] + }, + { + "description": "unevaluatedProperties.json: Evaluated properties collection needs to consider instance location with patternProperties", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "patternProperties": { + "^bar$": { + "type": "string" + } + } + } }, + "unevaluatedProperties": false + }, + "tests": [ { - "description": "object is valid", + "description": "with only the nested evaluated property", "data": { - "foo": 123 + "foo": { + "bar": "foo" + } }, "valid": true }, { - "description": "null is valid", - "data": null, - "valid": true + "description": "with an unevaluated property that exists at another location", + "data": { + "foo": { + "bar": "foo" + }, + "bar": "bar" + }, + "valid": false + } + ] + }, + { + "description": "unevaluatedProperties.json: Evaluated properties collection needs to consider instance location with additionalProperties", + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "additionalProperties": { + "type": "string" + } + } }, + "unevaluatedProperties": false + }, + "tests": [ { - "description": "number is invalid", - "data": 123, - "valid": false + "description": "with only the nested evaluated property", + "data": { + "foo": { + "bar": "foo" + } + }, + "valid": true }, { - "description": "string is invalid", - "data": "foo", + "description": "with an unevaluated property that exists at another location", + "data": { + "foo": { + "bar": "foo" + }, + "bar": "bar" + }, "valid": false } ] diff --git a/packages/ack/test/json_schema_2020_12_full_conformance_test.dart b/packages/ack/test/json_schema_2020_12_full_conformance_test.dart new file mode 100644 index 00000000..2c1e1d16 --- /dev/null +++ b/packages/ack/test/json_schema_2020_12_full_conformance_test.dart @@ -0,0 +1,57 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:ack/ack.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + const fixtureRoot = 'test/fixtures/json_schema_2020_12_full'; + final remoteRoot = Directory('$fixtureRoot/remotes/draft2020-12'); + final documents = { + for (final file in remoteRoot.listSync(recursive: true).whereType()) + if (file.path.endsWith('.json')) + Uri.parse( + 'http://localhost:1234/draft2020-12/' + '${p.relative(file.path, from: remoteRoot.path).replaceAll('\\', '/')}', + ): jsonDecode( + file.readAsStringSync(), + )!, + }; + final corpus = Directory('$fixtureRoot/draft2020-12'); + + group('official draft 2020-12 complete required suite', () { + for (final file in corpus.listSync().whereType()) { + if (!file.path.endsWith('.json')) continue; + final filename = p.basename(file.path); + final groups = (jsonDecode(file.readAsStringSync()) as List) + .cast>(); + for (final group in groups) { + test('$filename: ${group['description']}', () { + final schema = Ack.fromJsonSchema( + group['schema']!, + baseUri: Uri.parse( + 'http://localhost:1234/draft2020-12/__case__.json', + ), + documents: documents, + ); + final roundTrip = Ack.fromJsonSchema(schema.toJsonSchema()); + for (final example in group['tests']! as List) { + final testCase = example as Map; + final expected = testCase['valid'] as bool; + expect( + schema.safeParse(testCase['data']).isOk, + expected, + reason: testCase['description'] as String, + ); + expect( + roundTrip.safeParse(testCase['data']).isOk, + expected, + reason: 'export: ${testCase['description']}', + ); + } + }); + } + } + }); +} diff --git a/packages/ack/test/json_schema_dynamic_ref_conformance_test.dart b/packages/ack/test/json_schema_dynamic_ref_conformance_test.dart new file mode 100644 index 00000000..72e4c742 --- /dev/null +++ b/packages/ack/test/json_schema_dynamic_ref_conformance_test.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:ack/ack.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + const fixtureRoot = 'test/fixtures/json_schema_2020_12_full'; + final groups = >[ + for (final filename in [ + 'dynamicRef.json', + 'unevaluatedProperties.json', + 'unevaluatedItems.json', + ]) + for (final group + in (jsonDecode( + File( + '$fixtureRoot/draft2020-12/$filename', + ).readAsStringSync(), + ) + as List) + .cast>()) + if (filename == 'dynamicRef.json' || + (group['description'] as String).contains(r'$dynamicRef')) + {...group, 'description': '$filename: ${group['description']}'}, + ]; + final remotes = Directory('$fixtureRoot/remotes/draft2020-12'); + final documents = { + for (final file in remotes.listSync(recursive: true).whereType()) + if (file.path.endsWith('.json')) + Uri.parse( + 'http://localhost:1234/draft2020-12/' + '${p.relative(file.path, from: remotes.path).replaceAll('\\', '/')}', + ): jsonDecode( + file.readAsStringSync(), + )!, + }; + + group('official 2020-12 dynamicRef validation', () { + for (final group in groups) { + test(group['description']! as String, () { + final schema = Ack.fromJsonSchema( + group['schema']!, + baseUri: Uri.parse( + 'http://localhost:1234/draft2020-12/__dynamic_ref_test__.json', + ), + documents: documents, + ); + final exported = Ack.fromJsonSchema(schema.toJsonSchema()); + for (final example in group['tests']! as List) { + final value = example['data']; + final expected = example['valid'] as bool; + final reason = example['description'] as String; + expect(schema.safeParse(value).isOk, expected, reason: reason); + expect(schema.safeEncode(value).isOk, expected, reason: reason); + expect(exported.safeParse(value).isOk, expected, reason: reason); + } + }); + } + }); +} diff --git a/packages/ack/test/json_schema_format_conformance_test.dart b/packages/ack/test/json_schema_format_conformance_test.dart new file mode 100644 index 00000000..bebdfbf4 --- /dev/null +++ b/packages/ack/test/json_schema_format_conformance_test.dart @@ -0,0 +1,96 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:ack/ack.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + const root = 'test/fixtures/json_schema_2020_12_full/draft2020-12'; + final formatDirectory = Directory('$root/optional/format'); + + group('official 2020-12 optional format assertion', () { + for (final file in formatDirectory.listSync().whereType()) { + if (!file.path.endsWith('.json')) continue; + final filename = p.basename(file.path); + final groups = (jsonDecode(file.readAsStringSync()) as List) + .cast>(); + for (final group in groups) { + test('$filename: ${group['description']}', () { + if (filename == 'unknown.json') { + expect( + () => Ack.fromJsonSchema(group['schema']!, assertFormats: true), + throwsA(isA()), + ); + return; + } + final schema = Ack.fromJsonSchema( + group['schema']!, + assertFormats: true, + ); + final roundTrip = Ack.fromJsonSchema(schema.toJsonSchema()); + final mismatches = []; + for (final example in group['tests']! as List) { + final testCase = example as Map; + final actual = schema.safeParse(testCase['data']).isOk; + if (actual != testCase['valid']) { + mismatches.add( + '${testCase['description']}: expected ${testCase['valid']}, got $actual', + ); + } + final exported = roundTrip.safeParse(testCase['data']).isOk; + if (exported != testCase['valid']) { + mismatches.add( + 'export: ${testCase['description']}: expected ${testCase['valid']}, got $exported', + ); + } + } + expect(mismatches, isEmpty); + }); + } + } + }); + + group('official 2020-12 format-assertion vocabulary', () { + final groups = + (jsonDecode( + File( + '$root/optional/format-assertion.json', + ).readAsStringSync(), + ) + as List) + .cast>(); + final documents = { + for (final value in ['true', 'false']) + Uri.parse( + 'http://localhost:1234/draft2020-12/format-assertion-$value.json', + ): jsonDecode( + File( + '$root/../remotes/draft2020-12/format-assertion-$value.json', + ).readAsStringSync(), + )!, + }; + for (final group in groups) { + test(group['description']! as String, () { + final schema = Ack.fromJsonSchema( + group['schema']!, + documents: documents, + ); + final roundTrip = Ack.fromJsonSchema(schema.toJsonSchema()); + for (final example in group['tests']! as List) { + final testCase = example as Map; + expect( + schema.safeParse(testCase['data']).isOk, + testCase['valid'], + reason: testCase['description'] as String, + ); + expect( + roundTrip.safeParse(testCase['data']).isOk, + testCase['valid'], + reason: 'export: ${testCase['description']}', + ); + } + }); + } + }); +} diff --git a/packages/ack/test/json_schema_format_import_test.dart b/packages/ack/test/json_schema_format_import_test.dart deleted file mode 100644 index 7705464c..00000000 --- a/packages/ack/test/json_schema_format_import_test.dart +++ /dev/null @@ -1,162 +0,0 @@ -import 'package:ack/ack.dart'; -import 'package:test/test.dart'; - -void main() { - group('JSON Schema date-time format import', () { - test('strict import asserts date-time without an option', () { - final schema = Ack.fromJsonSchema({'format': 'date-time'}); - expect(schema.safeParse('2024-02-29T00:00:00Z').isOk, isTrue); - expect(schema.safeParse('2024-02-30T00:00:00Z').isFail, isTrue); - }); - - test('unsupported assertion reports its source pointer', () { - final base = Uri.parse('https://example.test/root.json'); - try { - Ack.fromJsonSchema( - { - 'properties': { - 'stamp': {r'$ref': r'formats.json#/$defs/stamp'}, - }, - }, - baseUri: base, - documents: { - Uri.parse('formats.json'): { - r'$defs': { - 'stamp': {'format': 'hostname'}, - }, - }, - }, - ); - fail('Expected an unsupported format diagnostic.'); - } on JsonSchemaImportException catch (error) { - final issue = error.diagnostics.single; - expect(issue.code, 'unsupported_keyword'); - expect(issue.keyword, 'format'); - expect(issue.pointer, r'#/$defs/stamp/format'); - expect(issue.documentUri, base.resolve('formats.json')); - } - - // Only reachable definitions are compiled. - expect( - Ack.fromJsonSchema({ - r'$defs': { - 'unused': {'format': 'hostname'}, - }, - }).safeParse('anything').isOk, - isTrue, - ); - }); - - test('non-string format values are invalid schema input', () { - expect( - () => Ack.fromJsonSchema({'format': 17}), - throwsA( - isA().having( - (error) => error.diagnostics.single.pointer, - 'pointer', - '#/format', - ), - ), - ); - }); - - test('format applies only to strings and preserves valid input', () { - final schema = Ack.fromJsonSchema({'format': 'date-time'}); - for (final value in [ - null, - true, - 4, - 1.25, - [], - {}, - ]) { - expect(schema.safeParse(value).isOk, isTrue, reason: '$value'); - } - const original = '1998-12-31t15:59:60.123456789-08:00'; - expect(schema.parse(original), original); - expect(schema.safeParse('2024-02-30T01:02:03Z').isFail, isTrue); - - final typed = Ack.fromJsonSchema({ - 'type': 'string', - 'format': 'date-time', - }); - expect(typed.safeParse(4).isFail, isTrue); - expect(typed.safeParse(original).isOk, isTrue); - }); - - test('assertion survives references, siblings, arrays and export', () { - final schema = Ack.fromJsonSchema({ - r'$defs': { - 'stamp': {'format': 'date-time'}, - }, - 'properties': { - 'stamps': { - 'type': 'array', - 'items': { - r'$ref': r'#/$defs/stamp', - 'allOf': [ - {'type': 'string'}, - ], - }, - }, - }, - }); - final exported = schema.toJsonSchema(); - final roundTrip = Ack.fromJsonSchema(exported); - for (final imported in [schema, roundTrip]) { - expect( - imported.safeParse({ - 'stamps': ['2024-02-29T00:00:00Z'], - }).isOk, - isTrue, - ); - expect( - imported.safeParse({ - 'stamps': ['2024-02-30T00:00:00Z'], - }).isFail, - isTrue, - ); - expect( - imported.safeParse({ - 'stamps': [7], - }).isFail, - isTrue, - ); - } - }); - - test('native date-time export can be asserted on import', () { - final native = Ack.string().datetime(); - final imported = Ack.fromJsonSchema(native.toJsonSchema()); - expect(imported.safeParse('2024-02-29T00:00:00Z').isOk, isTrue); - expect(imported.safeParse('2024-02-30T00:00:00Z').isFail, isTrue); - expect(imported.safeParse(4).isFail, isTrue); - }); - - test('checks dates, offsets and leap seconds in UTC', () { - final schema = Ack.fromJsonSchema({'format': 'date-time'}); - for (final value in [ - '2023-02-29T00:00:00Z', - '2024-02-29T00:00:00', - '2024-02-29T00:00:00+24:00', - '2024-02-29T00:00:00+01:60', - '2030-06-30T23:59:60Z', - '1998-12-31T15:58:60-08:00', - '1999-01-01T23:59:60Z', - '2024-02-29T00:00:00Z😀', - ]) { - expect(schema.safeParse(value).isFail, isTrue, reason: value); - } - for (final value in [ - '2024-02-29T00:00:00Z', - '2024-02-29t00:00:00.123456789z', - '1998-12-31T23:59:60Z', - '1998-12-31T15:59:60-08:00', - '1998-12-31t23:59:60z', - '1999-01-01t00:59:60+01:00', - ]) { - expect(schema.safeParse(value).isOk, isTrue, reason: value); - } - }); - }); -} diff --git a/packages/ack/test/json_schema_import_a2ui_test.dart b/packages/ack/test/json_schema_import_a2ui_test.dart index ab88b766..4834165b 100644 --- a/packages/ack/test/json_schema_import_a2ui_test.dart +++ b/packages/ack/test/json_schema_import_a2ui_test.dart @@ -33,42 +33,44 @@ void main() { } }); - test( - 'client capabilities reject meta-schema references with diagnostics', - () { - expect( - () => load('client_capabilities.json'), - throwsA( - isA().having( - (error) => error.diagnostics.map((issue) => issue.code), - 'diagnostic codes', - everyElement('unsupported_reference'), - ), - ), - ); - }, - ); + test('client capabilities validate embedded schemas offline', () { + final schema = load('client_capabilities.json'); + Map withComponentType(String type) => { + 'v0.9': { + 'supportedCatalogIds': [], + 'inlineCatalogs': [ + { + 'catalogId': 'example', + 'components': { + 'Button': {'type': type}, + }, + }, + ], + }, + }; + expect(schema.safeParse(withComponentType('string')).isOk, isTrue); + expect(schema.safeParse(withComponentType('invalid')).isFail, isTrue); + }); - test('client events assert the action timestamp format', () { + test('client event date-time format is an annotation by default', () { final schema = load('client_to_server.json'); - final event = { + final message = { 'version': 'v0.9', 'action': { - 'name': 'submit', - 'surfaceId': 'surface-1', - 'sourceComponentId': 'button-1', - 'timestamp': '2024-02-29T01:02:03Z', + 'name': 'click', + 'surfaceId': 'surface', + 'sourceComponentId': 'button', + 'timestamp': 'not-a-date', 'context': {}, }, }; - expect(schema.safeParse(event).isOk, isTrue); - final invalid = { - ...event, - 'action': { - ...(event['action']! as Map), - 'timestamp': '2024-02-30T01:02:03Z', - }, - }; - expect(schema.safeParse(invalid).isFail, isTrue); + expect(schema.safeParse(message).isOk, isTrue); + expect( + schema.safeParse({ + ...message, + 'action': {...message['action']! as Map, 'timestamp': 1}, + }).isFail, + isTrue, + ); }); } diff --git a/packages/ack/test/json_schema_import_api_test.dart b/packages/ack/test/json_schema_import_api_test.dart index 4f67d4c5..762f9b10 100644 --- a/packages/ack/test/json_schema_import_api_test.dart +++ b/packages/ack/test/json_schema_import_api_test.dart @@ -10,10 +10,23 @@ void main() { final Map modelExport = schema .toSchemaModel() .toJsonSchema(); + final Map draft7Export = schema.toJsonSchemaDraft7(); - expect(exported['type'], isNull); - expect(exported['definitions'], isA>()); + expect( + exported[r'$schema'], + 'https://json-schema.org/draft/2020-12/schema', + ); + expect(exported['type'], 'string'); + expect(exported['minLength'], 2); expect(modelExport, exported); + expect(draft7Export['type'], isNull); + expect(draft7Export['allOf'], [ + {r'$ref': '#/definitions/_ack_import_0_0'}, + ]); + expect((draft7Export['definitions'] as Map)['_ack_import_0_0'], { + 'type': 'string', + 'minLength': 2, + }); expect(schema.safeParse('Ada').isOk, isTrue); expect(schema.safeParse('A').isFail, isTrue); }); @@ -90,17 +103,19 @@ void main() { } }); - test('unsupported assertions expose immutable diagnostics', () { - final document = {'type': 'string', 'format': 'hostname'}; + test('unknown required vocabularies expose immutable diagnostics', () { + final document = { + r'$vocabulary': {'https://example.test/required': true}, + }; late JsonSchemaImportException failure; try { Ack.fromJsonSchema(document); - fail('Expected an unsupported keyword failure.'); + fail('Expected an unsupported vocabulary failure.'); } on JsonSchemaImportException catch (error) { failure = error; } - expect(failure.diagnostics.single.code, 'unsupported_keyword'); - expect(failure.diagnostics.single.pointer, '#/format'); + expect(failure.diagnostics.single.code, 'unsupported_vocabulary'); + expect(failure.diagnostics.single.pointer, r'#/$vocabulary'); expect(() => failure.diagnostics.clear(), throwsUnsupportedError); }); @@ -111,7 +126,7 @@ void main() { {'minLength': -1}, {r'$ref': 'missing.json'}, {r'$ref': '#'}, - {r'$schema': 'http://json-schema.org/draft-04/schema#'}, + {r'$schema': 'http://json-schema.org/draft-07/schema#'}, ]) { expect( () => Ack.fromJsonSchema(document), @@ -122,4 +137,16 @@ void main() { expect(never.safeParse(null).isFail, isTrue); expect(never.safeParse('anything').isFail, isTrue); }); + + test('dynamic references export by default and reject Draft-7 lowering', () { + final schema = Ack.fromJsonSchema({ + r'$dynamicAnchor': 'node', + 'type': 'object', + 'properties': { + 'next': {r'$dynamicRef': '#node'}, + }, + }); + expect(schema.toJsonSchema()[r'$dynamicAnchor'], 'node'); + expect(schema.toJsonSchemaDraft7, throwsUnsupportedError); + }); } diff --git a/packages/ack/test/json_schema_import_conformance_test.dart b/packages/ack/test/json_schema_import_conformance_test.dart index 6ea7ac15..698fabfc 100644 --- a/packages/ack/test/json_schema_import_conformance_test.dart +++ b/packages/ack/test/json_schema_import_conformance_test.dart @@ -35,32 +35,4 @@ void main() { }); } }); - - test('asserted date-time matches the official format fixture', () { - final fixture = - jsonDecode( - File( - 'test/fixtures/json_schema_2020_12_full/' - 'draft2020-12/optional/format/date-time.json', - ).readAsStringSync(), - ) - as List; - final imported = Ack.fromJsonSchema({'format': 'date-time'}); - final native = Ack.string().datetime(); - var count = 0; - for (final group in fixture.cast>()) { - for (final probe - in (group['tests'] as List).cast>()) { - final value = probe['data']; - final expected = probe['valid'] as bool; - final reason = probe['description'] as String; - expect(imported.safeParse(value).isOk, expected, reason: reason); - if (value is String) { - expect(native.safeParse(value).isOk, expected, reason: reason); - } - count++; - } - } - expect(count, 43); - }); } diff --git a/packages/ack/test/json_schema_import_test.dart b/packages/ack/test/json_schema_import_test.dart index dae3c593..94e58511 100644 --- a/packages/ack/test/json_schema_import_test.dart +++ b/packages/ack/test/json_schema_import_test.dart @@ -52,6 +52,23 @@ void main() { } }); + test('rejects a supplied boolean dialect with an import diagnostic', () { + final dialect = Uri.parse('https://example.test/boolean-dialect'); + expect( + () => Ack.fromJsonSchema( + {r'$schema': dialect.toString()}, + documents: {dialect: true}, + ), + throwsA( + isA().having( + (error) => error.diagnostics.single.code, + 'diagnostic code', + 'unsupported_dialect', + ), + ), + ); + }); + test( 'rejects invalid annotation shapes instead of emitting invalid schemas', () { @@ -73,31 +90,23 @@ void main() { ); test( - 'added constraints do not become ignored Draft-7 reference siblings', + 'added constraints remain effective beside 2020-12 source schemas', () { final schema = Ack.fromJsonSchema(true).constrain(_OnlyStrings()); expect(schema.safeParse('ok').isOk, isTrue); expect(schema.safeParse(1).isFail, isTrue); expect(schema.safeParse(null).isOk, isTrue); final exported = schema.toJsonSchema(); - // Draft-7 ignores every sibling of $ref. A root reference here would - // silently discard the additional, exported `type: string` assertion. expect(exported.containsKey(r'$ref'), isFalse); + expect( + exported[r'$schema'], + 'https://json-schema.org/draft/2020-12/schema', + ); final branches = exported['anyOf'] as List; final constrained = branches .whereType>() .singleWhere((branch) => branch['type'] == 'string'); expect(constrained, isNot(contains(r'$ref'))); - final referenceEnvelope = constrained['allOf'] as List; - expect(referenceEnvelope, hasLength(1)); - expect( - referenceEnvelope.single, - isA>().having( - (reference) => reference[r'$ref'], - r'$ref', - isA(), - ), - ); expect(Ack.fromJsonSchema(exported).safeParse(1).isFail, isTrue); expect(Ack.fromJsonSchema(exported).safeParse(null).isOk, isTrue); expect(schema.toSchemaModel().toJsonSchema(), exported); @@ -113,6 +122,70 @@ void main() { expect(schema.describe('Override').description, 'Override'); }); + test('nullable export retains local pointers into the source resource', () { + final schema = Ack.fromJsonSchema({ + 'type': 'object', + 'properties': { + 'name': {'type': 'string'}, + 'alias': {r'$ref': '#/properties/name'}, + }, + }).nullable(); + final restored = Ack.fromJsonSchema(schema.toJsonSchema()); + for (final value in [ + null, + {'name': 'Ada', 'alias': 'Ada'}, + {'name': 'Ada', 'alias': 1}, + ]) { + expect(restored.safeParse(value).isOk, schema.safeParse(value).isOk); + } + }); + + test('nullable export keeps recursive references scoped to the source', () { + final schema = Ack.fromJsonSchema({ + 'type': 'object', + 'properties': { + 'child': {r'$ref': '#'}, + }, + }).nullable(); + final restored = Ack.fromJsonSchema(schema.toJsonSchema()); + for (final value in [ + null, + {}, + {'child': {}}, + {'child': null}, + ]) { + expect(restored.safeParse(value).isOk, schema.safeParse(value).isOk); + } + }); + + test('nullable export keeps root anchors scoped to the source', () { + for (final (anchor, reference) in [ + (r'$anchor', r'$ref'), + (r'$dynamicAnchor', r'$dynamicRef'), + ]) { + final schema = Ack.fromJsonSchema({ + anchor: 'node', + 'type': 'object', + 'properties': { + 'child': {reference: '#node'}, + }, + }).nullable(); + final restored = Ack.fromJsonSchema(schema.toJsonSchema()); + for (final value in [ + null, + {}, + {'child': {}}, + {'child': null}, + ]) { + expect( + restored.safeParse(value).isOk, + schema.safeParse(value).isOk, + reason: '$anchor: $value', + ); + } + } + }); + test( 'resolves catalog schemas stored outside standard schema containers', () { @@ -164,7 +237,7 @@ void main() { expect(schema.safeParse('hello').isOk, isTrue); }); - test('empty and repeated enum values export as valid Draft-7 schemas', () { + test('empty and repeated enum values retain 2020-12 semantics', () { final never = Ack.fromJsonSchema({'enum': []}); for (final value in [null, 1, 'x', [], {}]) { expect(never.safeParse(value).isFail, isTrue); @@ -177,10 +250,8 @@ void main() { 'enum': [1, 1.0, 'x', 'x'], }); expect(repeated.safeParse(1).isOk, isTrue); - final definitions = repeated.toJsonSchema()['definitions'] as Map; - expect((definitions.values.first as Map)['enum'], [1, 'x']); - final falseDefinitions = never.toJsonSchema()['definitions'] as Map; - expect(falseDefinitions.values.first, {'not': {}}); + expect(repeated.toJsonSchema()['enum'], [1, 1.0, 'x', 'x']); + expect(never.toJsonSchema()['enum'], isEmpty); }); test('pattern and propertyNames validate and export verbatim', () { @@ -200,35 +271,47 @@ void main() { expect(schema.safeParse({'name': 42}).isOk, isTrue); final export = schema.toJsonSchema(); - final definitions = (export['definitions'] as Map).values.cast(); - expect(definitions.any((d) => d['pattern'] == r'^a+$'), isTrue); - expect( - definitions.any((d) => (d['propertyNames'] as Map?)?[r'$ref'] != null), - isTrue, - ); + expect((export['properties'] as Map)['name'], {'pattern': r'^a+$'}); + expect(export['propertyNames'], {'maxLength': 4}); final roundTrip = Ack.fromJsonSchema(export); expect(roundTrip.safeParse(accepted).isOk, isTrue); expect(roundTrip.safeParse(badPattern).isFail, isTrue); expect(roundTrip.safeParse(badPropertyName).isFail, isTrue); }); - test('pattern compiles in ECMA-262 Unicode mode', () { - final single = Ack.fromJsonSchema({'pattern': r'^.$'}); - expect(single.safeParse('😀').isOk, isTrue); - expect(single.safeParse('😀😀').isFail, isTrue); - + test('Unicode property escapes use ECMA-262 Unicode matching', () { + final schema = Ack.fromJsonSchema({ + 'type': 'string', + 'pattern': r'^\p{Letter}+$', + }); + expect(schema.safeParse('Éclair').isOk, isTrue); + expect(schema.safeParse('東京').isOk, isTrue); + expect(schema.safeParse('123').isFail, isTrue); expect( - () => Ack.fromJsonSchema({'pattern': r'^\d{3}\-\d{4}$'}), - throwsA( - isA().having( - (e) => e.diagnostics.single.pointer, - 'pointer', - '#/pattern', - ), - ), + Ack.fromJsonSchema(schema.toJsonSchema()).safeParse('東京').isOk, + isTrue, ); }); + test('multipleOf compares decimal values exactly', () { + final schema = Ack.fromJsonSchema({'type': 'number', 'multipleOf': 0.1}); + for (final value in [0, 0.3, -0.3, 1.0, 1e2]) { + expect(schema.safeParse(value).isOk, isTrue, reason: '$value'); + } + for (final value in [0.31, 1.2000000000000002, 1e-7]) { + expect(schema.safeParse(value).isFail, isTrue, reason: '$value'); + } + final restored = Ack.fromJsonSchema(schema.toJsonSchema()); + expect(restored.safeParse(0.3).isOk, isTrue); + expect(restored.safeParse(0.31).isFail, isTrue); + for (final invalid in [0, -0.1, '0.1']) { + expect( + () => Ack.fromJsonSchema({'multipleOf': invalid}), + throwsA(isA()), + ); + } + }); + test('reference failures include source URI and keyword location', () { try { Ack.fromJsonSchema({ @@ -250,34 +333,13 @@ void main() { {r'$ref': '#/%FF'}, {r'$schema': null}, {r'$anchor': null}, - {r'$schema': 'http://json-schema.org/draft-04/schema#'}, + {r'$schema': 'http://json-schema.org/draft-07/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', () { @@ -366,200 +428,471 @@ void main() { expect(error.toMap()['value'], 'foobar'); }); - test('runtime errors expose the failing keyword and its location', () { + test('recursive validation has no hidden Ack.lazy depth limit', () { final schema = Ack.fromJsonSchema({ 'type': 'object', 'properties': { - 'tags': { - 'type': 'array', - 'items': { - 'enum': ['x', 'y'], - }, - }, - }, - }); - final error = - schema.safeParse({ - 'tags': ['x', 'z'], - }).getError() - as JsonSchemaValidationError; - expect(error.keyword, 'enum'); - expect(error.documentUri, Uri.parse('ack-import:///root.json')); - expect(error.pointer, '#/properties/tags/items/enum'); - expect(error.path, '#/tags/1'); - expect( - error.toMap(), - allOf( - containsPair('keyword', 'enum'), - containsPair('documentUri', 'ack-import:///root.json'), - containsPair('pointer', '#/properties/tags/items/enum'), - ), - ); - }); - - test('runtime error locations follow references across documents', () { - final schema = Ack.fromJsonSchema( - { - 'properties': { - 'a': {r'$ref': r'defs.json#/$defs/a~1b'}, - 'b': { - 'properties': {'c': false}, - }, - }, - 'required': ['a'], - }, - baseUri: Uri.parse('https://example.test/root.json'), - documents: { - Uri.parse('https://example.test/defs.json'): { - r'$defs': { - 'a/b': {'type': 'string'}, - }, - }, + 'child': {r'$ref': '#'}, }, - ); - JsonSchemaValidationError failure(Object value) => - schema.safeParse(value).getError() as JsonSchemaValidationError; - - final type = failure({'a': 1}); - expect(type.keyword, 'type'); - expect(type.documentUri, Uri.parse('https://example.test/defs.json')); - expect(type.pointer, r'#/$defs/a~1b/type'); - expect(type.path, '#/a'); - - final rejected = failure({ - 'a': 'ok', - 'b': {'c': 1}, }); - expect(rejected.keyword, isEmpty); - expect(rejected.documentUri, Uri.parse('https://example.test/root.json')); - expect(rejected.pointer, '#/properties/b/properties/c'); - expect(rejected.message, contains('"false" failed')); - - final missing = failure({}); - expect(missing.keyword, 'required'); - expect(missing.pointer, '#/required'); - expect(missing.path, '#/a'); + var value = {}; + for (var i = 0; i < 150; i++) { + value = {'child': value}; + } + expect(schema.safeParse(value).isOk, isTrue); }); - group('error locations use URI fragment encoding', () { - for (final entry in { - '%': '%25', - '%2F': '%252F', - ' ': '%20', - '#': '%23', - '☃😀': '%E2%98%83%F0%9F%98%80', - '/': '~1', - '~': '~0', - '~1': '~01', - }.entries) { - test('runtime keyword location for "${entry.key}"', () { + test( + 'URI fragments are decoded once, including percent and tilde tokens', + () { + for (final entry in { + r'#/$defs/a%20b': 'a b', + r'#/$defs/a%2520b': 'a%20b', + r'#/$defs/%E2%98%83': '☃', + r'#/$defs/~01': '~1', + }.entries) { final schema = Ack.fromJsonSchema({ - 'properties': { - entry.key: {'type': 'string'}, + r'$defs': { + entry.value: {'const': 'yes'}, }, + r'$ref': entry.key, }); - expect(schema.safeParse({entry.key: 'ok'}).isOk, isTrue); - for (final result in [ - schema.safeParse({entry.key: 42}), - schema.safeEncode({entry.key: 42}), - ]) { - final error = result.getError() as JsonSchemaValidationError; - final pointer = '#/properties/${entry.value}/type'; - expect(error.keyword, 'type'); - expect(error.pointer, pointer); - expect(error.toMap(), containsPair('pointer', pointer)); - expect(error.value, 42); - } - }); + expect(schema.safeParse('yes').isOk, isTrue, reason: entry.key); + expect(schema.safeParse('no').isFail, isTrue, reason: entry.key); + } + }, + ); - test('referenced false schema location for "${entry.key}"', () { - final documentUri = Uri.parse('https://example.test/defs.json'); - final schema = Ack.fromJsonSchema( - {r'$ref': 'child.json#/properties/${entry.value}'}, - baseUri: Uri.parse('https://example.test/root.json'), - documents: { - documentUri: { - r'$defs': { - 'resource': { - r'$id': 'child.json', - 'properties': {entry.key: false}, - }, + test( + 'nested IDs change ref scope without scanning literal data for IDs', + () { + final schema = Ack.fromJsonSchema( + { + r'$id': 'https://example.test/root.json', + r'$defs': { + 'nested': { + r'$id': 'child/index.json', + 'properties': { + 'x': {r'$ref': 'value.json'}, }, + 'required': ['x'], }, }, - ); - final error = - schema.safeParse(42).getError() as JsonSchemaValidationError; - expect(error.keyword, isEmpty); - expect(error.documentUri, documentUri); - expect(error.pointer, '#/\$defs/resource/properties/${entry.value}'); - expect(error.path, '#'); - }); - - test('import diagnostic location for "${entry.key}"', () { - expect( - () => Ack.fromJsonSchema({ - 'properties': { - entry.key: {'type': 'invalid'}, - }, - }), - throwsA( - isA().having( - (error) => error.diagnostics.single.pointer, - 'pointer', - '#/properties/${entry.value}/type', - ), - ), - ); - }); - } - - test('root false schema keeps the empty fragment', () { - final error = - Ack.fromJsonSchema(false).safeParse(42).getError() - as JsonSchemaValidationError; - expect(error.keyword, isEmpty); - expect(error.pointer, '#'); - }); - }); - - group('keywordLocation follows the evaluation path', () { - const draft = 'https://json-schema.org/draft/2020-12/schema'; - String keywordLocation(Map source, Object value) => - (Ack.fromJsonSchema(source).safeParse(value).getError() - as JsonSchemaValidationError) - .keywordLocation; - - test('separates propertyNames from additionalProperties', () { - final schema = Ack.fromJsonSchema({ - r'$schema': draft, - r'$defs': { - 'short': {'type': 'string', 'maxLength': 3}, + r'$ref': 'child/index.json', + 'default': {r'$id': 'child/index.json'}, }, - 'properties': { - 'names': { - 'propertyNames': {r'$ref': r'#/$defs/short'}, - }, - 'values': { - 'additionalProperties': {r'$ref': r'#/$defs/short'}, + documents: { + Uri.parse('https://example.test/child/value.json'): { + 'type': 'boolean', }, }, - }); - JsonSchemaValidationError failure(Object value) => - schema.safeParse(value).getError() as JsonSchemaValidationError; + ); + expect(schema.safeParse({'x': true}).isOk, isTrue); + expect(schema.safeParse({'x': 1}).isFail, isTrue); + }, + ); - final name = failure({ - 'names': {'abcd': 'x'}, - }); - final value = failure({ - 'values': {'abcd': 'abcd'}, - }); + test( + 'the supplied bundle may also contain the identical root document', + () { + final uri = Uri.parse('https://example.test/root.json'); + final document = {'type': 'string'}; expect( - name.keywordLocation, - r'/properties/names/propertyNames/$ref/maxLength', + Ack.fromJsonSchema( + document, + baseUri: uri, + documents: {uri: document}, + ).safeParse('ok').isOk, + isTrue, ); - expect( - value.keywordLocation, + }, + ); + + test('schema documents and parsed values are detached snapshots', () { + final types = ['string']; + final document = {'type': types}; + final schema = Ack.fromJsonSchema(document); + types.add('number'); + expect(schema.safeParse(1).isFail, isTrue); + final input = { + 'a': [1], + }; + final parsed = Ack.fromJsonSchema(true).parse(input) as Map; + input['a']!.add(2); + expect(parsed, { + 'a': [1], + }); + expect(() => (parsed['a'] as List).add(2), throwsUnsupportedError); + final cycle = []; + cycle.add(cycle); + expect(Ack.fromJsonSchema(true).safeParse(cycle).isFail, isTrue); + expect(Ack.fromJsonSchema(true).safeParse(double.nan).isFail, isTrue); + }); + + test( + 'fluent nullability overrides preserve parse/encode/export parity', + () { + for (final schema in [ + Ack.fromJsonSchema({'type': 'string'}).nullable(), + Ack.fromJsonSchema(true).nullable(value: false), + ]) { + final roundTrip = Ack.fromJsonSchema(schema.toJsonSchema()); + expect(schema.safeParse(null).isOk, schema.isNullable); + expect(schema.safeEncode(null).isOk, schema.isNullable); + expect(roundTrip.safeParse(null).isOk, schema.isNullable); + } + }, + ); + + test( + 'resolves cross-file, escaped pointers, anchors, and recursive refs', + () { + final schema = Ack.fromJsonSchema( + { + r'$id': 'https://example.test/root.json', + r'$ref': 'types.json#node', + }, + documents: { + Uri.parse('https://example.test/types.json'): { + r'$defs': { + 'a/b~c': { + r'$anchor': 'node', + 'type': 'object', + 'properties': { + 'value': {'type': 'integer'}, + 'next': {r'$ref': r'#/$defs/a~1b~0c'}, + }, + 'required': ['value'], + 'additionalProperties': false, + }, + }, + }, + }, + ); + final value = { + 'value': 1, + 'next': {'value': 2}, + }; + expect(schema.parse(value), value); + expect(schema.safeParse({'value': 1, 'next': {}}).isFail, isTrue); + final roundTrip = Ack.fromJsonSchema(schema.toJsonSchema()); + expect(roundTrip.safeParse(value).isOk, isTrue); + expect(roundTrip.safeParse({'value': 1, 'next': {}}).isFail, isTrue); + }, + ); + + test( + 'reports missing refs and nonproductive cycles instead of overflowing', + () { + for (final document in [ + {r'$ref': 'missing.json'}, + {r'$ref': '#'}, + { + r'$defs': { + 'a': {r'$ref': '#'}, + }, + r'$ref': r'#/$defs/a', + }, + ]) { + expect( + () => Ack.fromJsonSchema(document), + throwsA(isA()), + ); + } + }, + ); + + test('bundled 2020-12 meta-schema validates offline', () { + final meta = Ack.fromJsonSchema({ + r'$ref': 'https://json-schema.org/draft/2020-12/schema', + }); + expect(meta.safeParse({'type': 'string'}).isOk, isTrue); + expect(meta.safeParse({'type': 'not-a-json-type'}).isFail, isTrue); + expect(meta.safeParse({'minimum': 'zero'}).isFail, isTrue); + }); + + test('all standard meta-schema resources resolve offline', () { + for (final name in [ + 'schema', + 'meta/core', + 'meta/applicator', + 'meta/unevaluated', + 'meta/validation', + 'meta/meta-data', + 'meta/format-annotation', + 'meta/format-assertion', + 'meta/content', + ]) { + final resource = Ack.fromJsonSchema({ + r'$ref': 'https://json-schema.org/draft/2020-12/$name', + }); + expect( + resource.safeParse({'type': 'string'}).isOk, + isTrue, + reason: name, + ); + } + }); + + test('unsupported standard keywords report escaped locations', () { + final document = { + 'properties': { + 'a/b': { + r'$vocabulary': {'https://example.test/required': true}, + }, + }, + }; + try { + Ack.fromJsonSchema(document); + fail('Expected unsupported keyword diagnostics.'); + } on JsonSchemaImportException catch (error) { + expect( + error.diagnostics.map((diagnostic) => diagnostic.pointer), + contains(r'#/properties/a~1b/$vocabulary'), + ); + } + }); + + test( + 'format, content, and unknown keywords are annotations by default', + () { + final document = { + 'type': 'string', + 'format': 'email', + 'contentEncoding': 'base64', + 'contentMediaType': 'application/json', + 'contentSchema': {'type': 'object'}, + 'x-custom': {'expected': false}, + }; + final schema = Ack.fromJsonSchema(document); + expect(schema.safeParse('not an email or base64').isOk, isTrue); + expect(schema.safeParse(1).isFail, isTrue); + final imported = schema.toJsonSchema(); + for (final key in [ + 'format', + 'contentEncoding', + 'contentMediaType', + 'contentSchema', + 'x-custom', + ]) { + expect(imported[key], document[key], reason: key); + } + expect( + Ack.fromJsonSchema(schema.toJsonSchema()).safeParse('invalid').isOk, + isTrue, + ); + }, + ); + + test('unknown required vocabularies remain unsupported', () { + expect( + () => Ack.fromJsonSchema({ + r'$vocabulary': {'https://example.test/required': true}, + }), + throwsA(isA()), + ); + }); + + test('unknown optional vocabularies are retained but inert', () { + final schema = Ack.fromJsonSchema({ + r'$vocabulary': {'https://example.test/optional': false}, + 'type': 'string', + }); + expect(schema.safeParse('accepted').isOk, isTrue); + expect(schema.safeParse(1).isFail, isTrue); + expect(schema.toJsonSchema()[r'$vocabulary'], { + 'https://example.test/optional': false, + }); + }); + + test('rejects malformed supported keywords', () { + for (final document in [ + {'type': 'made-up'}, + {'required': 'a'}, + {'minItems': -1}, + {'anyOf': []}, + {'enum': 1}, + {'items': 3}, + {'pattern': 1}, + {'pattern': '('}, + ]) { + expect( + () => Ack.fromJsonSchema(document), + throwsA(isA()), + reason: '$document', + ); + } + }); + + test( + 'runtime failures identify the failing keyword and source location', + () { + final schema = Ack.fromJsonSchema( + { + 'properties': { + 'a': {r'$ref': r'defs.json#/$defs/a~1b'}, + 'b': { + 'properties': {'c': false}, + }, + }, + 'required': ['a'], + }, + baseUri: Uri.parse('https://example.test/root.json'), + documents: { + Uri.parse('https://example.test/defs.json'): { + r'$defs': { + 'a/b': {'type': 'string'}, + }, + }, + }, + ); + JsonSchemaValidationError failure(Object value) => + schema.safeParse(value).getError() as JsonSchemaValidationError; + + final type = failure({'a': 1}); + expect(type.keyword, 'type'); + expect(type.documentUri, Uri.parse('https://example.test/defs.json')); + expect(type.pointer, r'#/$defs/a~1b/type'); + expect(type.path, '#/a'); + + final rejected = failure({ + 'a': 'ok', + 'b': {'c': 1}, + }); + expect(rejected.keyword, isEmpty); + expect( + rejected.documentUri, + Uri.parse('https://example.test/root.json'), + ); + expect(rejected.pointer, '#/properties/b/properties/c'); + expect(rejected.message, contains('"false" failed')); + + final missing = failure({}); + expect(missing.keyword, 'required'); + expect(missing.pointer, '#/required'); + expect(missing.path, '#/a'); + }, + ); + + group('error locations use URI fragment encoding', () { + for (final entry in { + '%': '%25', + '%2F': '%252F', + ' ': '%20', + '#': '%23', + '☃😀': '%E2%98%83%F0%9F%98%80', + '/': '~1', + '~': '~0', + '~1': '~01', + }.entries) { + test('runtime keyword location for "${entry.key}"', () { + final schema = Ack.fromJsonSchema({ + 'properties': { + entry.key: {'type': 'string'}, + }, + }); + expect(schema.safeParse({entry.key: 'ok'}).isOk, isTrue); + for (final result in [ + schema.safeParse({entry.key: 42}), + schema.safeEncode({entry.key: 42}), + ]) { + final error = result.getError() as JsonSchemaValidationError; + final pointer = '#/properties/${entry.value}/type'; + expect(error.keyword, 'type'); + expect(error.pointer, pointer); + expect(error.toMap(), containsPair('pointer', pointer)); + expect(error.value, 42); + } + }); + + test('referenced false schema location for "${entry.key}"', () { + final documentUri = Uri.parse('https://example.test/defs.json'); + final schema = Ack.fromJsonSchema( + {r'$ref': 'child.json#/properties/${entry.value}'}, + baseUri: Uri.parse('https://example.test/root.json'), + documents: { + documentUri: { + r'$defs': { + 'resource': { + r'$id': 'child.json', + 'properties': {entry.key: false}, + }, + }, + }, + }, + ); + final error = + schema.safeParse(42).getError() as JsonSchemaValidationError; + expect(error.keyword, isEmpty); + expect(error.documentUri, documentUri); + expect(error.pointer, '#/\$defs/resource/properties/${entry.value}'); + expect(error.path, '#'); + }); + + test('import diagnostic location for "${entry.key}"', () { + expect( + () => Ack.fromJsonSchema({ + 'properties': { + entry.key: {'type': 'invalid'}, + }, + }), + throwsA( + isA().having( + (error) => error.diagnostics.single.pointer, + 'pointer', + '#/properties/${entry.value}/type', + ), + ), + ); + }); + } + + test('root false schema keeps the empty fragment', () { + final error = + Ack.fromJsonSchema(false).safeParse(42).getError() + as JsonSchemaValidationError; + expect(error.keyword, isEmpty); + expect(error.pointer, '#'); + }); + }); + + group('keywordLocation follows the evaluation path', () { + const draft = 'https://json-schema.org/draft/2020-12/schema'; + String keywordLocation(Map source, Object value) => + (Ack.fromJsonSchema(source).safeParse(value).getError() + as JsonSchemaValidationError) + .keywordLocation; + + test('separates propertyNames from additionalProperties', () { + final schema = Ack.fromJsonSchema({ + r'$schema': draft, + r'$defs': { + 'short': {'type': 'string', 'maxLength': 3}, + }, + 'properties': { + 'names': { + 'propertyNames': {r'$ref': r'#/$defs/short'}, + }, + 'values': { + 'additionalProperties': {r'$ref': r'#/$defs/short'}, + }, + }, + }); + JsonSchemaValidationError failure(Object value) => + schema.safeParse(value).getError() as JsonSchemaValidationError; + + final name = failure({ + 'names': {'abcd': 'x'}, + }); + final value = failure({ + 'values': {'abcd': 'abcd'}, + }); + expect( + name.keywordLocation, + r'/properties/names/propertyNames/$ref/maxLength', + ); + expect( + value.keywordLocation, r'/properties/values/additionalProperties/$ref/maxLength', ); expect(name.pointer, value.pointer); @@ -614,268 +947,67 @@ void main() { expect( keywordLocation( { - 'items': { - 'anyOf': [ - {'type': 'string'}, - {'type': 'boolean'}, - ], - }, - }, - ['x', 1], - ), - '/items/anyOf', - ); - }); - - test('escapes reference tokens without percent-encoding', () { - expect( - keywordLocation( - { - 'properties': { - 'a/b': { - 'properties': { - '~ %': {'type': 'string'}, - }, - }, - }, - }, - { - 'a/b': {'~ %': 1}, - }, - ), - '/properties/a~1b/properties/~0 %/type', - ); - }); - - test('is empty when the root schema is false', () { - final error = - Ack.fromJsonSchema(false).safeParse(42).getError() - as JsonSchemaValidationError; - expect(error.keywordLocation, isEmpty); - expect(error.toMap(), containsPair('keywordLocation', '')); - }); - - test('appears in toMap', () { - final error = - Ack.fromJsonSchema({ - 'properties': { - 'a': {r'$ref': r'#/$defs/s'}, - }, - r'$defs': { - 's': {'type': 'string'}, - }, - }).safeParse({'a': 1}).getError() - as JsonSchemaValidationError; - expect( - error.toMap(), - allOf( - containsPair('keywordLocation', r'/properties/a/$ref/type'), - containsPair('pointer', r'#/$defs/s/type'), - ), - ); - }); - }); - - test('recursive validation has no hidden Ack.lazy depth limit', () { - final schema = Ack.fromJsonSchema({ - 'type': 'object', - 'properties': { - 'child': {r'$ref': '#'}, - }, - }); - var value = {}; - for (var i = 0; i < 150; i++) { - value = {'child': value}; - } - expect(schema.safeParse(value).isOk, isTrue); - }); - - test( - 'URI fragments are decoded once, including percent and tilde tokens', - () { - for (final entry in { - r'#/$defs/a%20b': 'a b', - r'#/$defs/a%2520b': 'a%20b', - r'#/$defs/%E2%98%83': '☃', - r'#/$defs/~01': '~1', - }.entries) { - final schema = Ack.fromJsonSchema({ - r'$defs': { - entry.value: {'const': 'yes'}, - }, - r'$ref': entry.key, - }); - expect(schema.safeParse('yes').isOk, isTrue, reason: entry.key); - expect(schema.safeParse('no').isFail, isTrue, reason: entry.key); - } - }, - ); - - test( - 'nested IDs change ref scope without scanning literal data for IDs', - () { - final schema = Ack.fromJsonSchema( - { - r'$id': 'https://example.test/root.json', - r'$defs': { - 'nested': { - r'$id': 'child/index.json', - 'properties': { - 'x': {r'$ref': 'value.json'}, - }, - 'required': ['x'], - }, - }, - r'$ref': 'child/index.json', - 'default': {r'$id': 'child/index.json'}, - }, - documents: { - Uri.parse('https://example.test/child/value.json'): { - 'type': 'boolean', - }, - }, - ); - expect(schema.safeParse({'x': true}).isOk, isTrue); - expect(schema.safeParse({'x': 1}).isFail, isTrue); - }, - ); - - test( - 'the supplied bundle may also contain the identical root document', - () { - final uri = Uri.parse('https://example.test/root.json'); - final document = {'type': 'string'}; - expect( - Ack.fromJsonSchema( - document, - baseUri: uri, - documents: {uri: document}, - ).safeParse('ok').isOk, - isTrue, - ); - }, - ); - - test('schema documents and parsed values are detached snapshots', () { - final types = ['string']; - final document = {'type': types}; - final schema = Ack.fromJsonSchema(document); - types.add('number'); - expect(schema.safeParse(1).isFail, isTrue); - final input = { - 'a': [1], - }; - final parsed = Ack.fromJsonSchema(true).parse(input) as Map; - input['a']!.add(2); - expect(parsed, { - 'a': [1], - }); - expect(() => (parsed['a'] as List).add(2), throwsUnsupportedError); - final cycle = []; - cycle.add(cycle); - expect(Ack.fromJsonSchema(true).safeParse(cycle).isFail, isTrue); - expect(Ack.fromJsonSchema(true).safeParse(double.nan).isFail, isTrue); - }); - - test('parse and encode reject the same non-JSON values', () { - final schema = Ack.fromJsonSchema(true); - final cycle = {}; - cycle['self'] = [cycle]; - for (final value in [ - {1: 'a'}, - { - 'a': [double.infinity], - }, - cycle, - [DateTime(2026)], - ]) { - final parsed = schema.safeParse(value); - final encoded = schema.safeEncode(value); - expect(parsed.isFail, isTrue, reason: '$value'); - expect(encoded.isFail, isTrue, reason: '$value'); - expect(parsed.getError().message, encoded.getError().message); - } - final shared = ['x']; - final parsed = schema.parse({'a': shared, 'b': shared}) as Map; - expect(parsed, { - 'a': ['x'], - 'b': ['x'], + 'items': { + 'anyOf': [ + {'type': 'string'}, + {'type': 'boolean'}, + ], + }, + }, + ['x', 1], + ), + '/items/anyOf', + ); }); - expect(() => parsed['c'] = 1, throwsUnsupportedError); - }); - - test( - 'fluent nullability overrides preserve parse/encode/export parity', - () { - for (final schema in [ - Ack.fromJsonSchema({'type': 'string'}).nullable(), - Ack.fromJsonSchema(true).nullable(value: false), - ]) { - final roundTrip = Ack.fromJsonSchema(schema.toJsonSchema()); - expect(schema.safeParse(null).isOk, schema.isNullable); - expect(schema.safeEncode(null).isOk, schema.isNullable); - expect(roundTrip.safeParse(null).isOk, schema.isNullable); - } - }, - ); - test( - 'resolves cross-file, escaped pointers, anchors, and recursive refs', - () { - final schema = Ack.fromJsonSchema( - { - r'$id': 'https://example.test/root.json', - r'$ref': 'types.json#node', - }, - documents: { - Uri.parse('https://example.test/types.json'): { - r'$defs': { - 'a/b~c': { - r'$anchor': 'node', - 'type': 'object', + test('escapes reference tokens without percent-encoding', () { + expect( + keywordLocation( + { + 'properties': { + 'a/b': { 'properties': { - 'value': {'type': 'integer'}, - 'next': {r'$ref': r'#/$defs/a~1b~0c'}, + '~ %': {'type': 'string'}, }, - 'required': ['value'], - 'additionalProperties': false, }, }, }, - }, + { + 'a/b': {'~ %': 1}, + }, + ), + '/properties/a~1b/properties/~0 %/type', ); - final value = { - 'value': 1, - 'next': {'value': 2}, - }; - expect(schema.parse(value), value); - expect(schema.safeParse({'value': 1, 'next': {}}).isFail, isTrue); - final roundTrip = Ack.fromJsonSchema(schema.toJsonSchema()); - expect(roundTrip.safeParse(value).isOk, isTrue); - expect(roundTrip.safeParse({'value': 1, 'next': {}}).isFail, isTrue); - }, - ); + }); - test( - 'reports missing refs and nonproductive cycles instead of overflowing', - () { - for (final document in [ - {r'$ref': 'missing.json'}, - {r'$ref': '#'}, - { - r'$defs': { - 'a': {r'$ref': '#'}, - }, - r'$ref': r'#/$defs/a', - }, - ]) { - expect( - () => Ack.fromJsonSchema(document), - throwsA(isA()), - ); - } - }, - ); + test('is empty when the root schema is false', () { + final error = + Ack.fromJsonSchema(false).safeParse(42).getError() + as JsonSchemaValidationError; + expect(error.keywordLocation, isEmpty); + expect(error.toMap(), containsPair('keywordLocation', '')); + }); + + test('appears in toMap', () { + final error = + Ack.fromJsonSchema({ + 'properties': { + 'a': {r'$ref': r'#/$defs/s'}, + }, + r'$defs': { + 's': {'type': 'string'}, + }, + }).safeParse({'a': 1}).getError() + as JsonSchemaValidationError; + expect( + error.toMap(), + allOf( + containsPair('keywordLocation', r'/properties/a/$ref/type'), + containsPair('pointer', r'#/$defs/s/type'), + ), + ); + }); + }); test('conditional reference cycles are nonproductive', () { for (final keyword in ['if', 'then', 'else']) { @@ -974,307 +1106,6 @@ void main() { expect(nested.value, ['a', 'b', 'a']); }, ); - - test( - 'reports meta-schema references as unsupported, not as any schema', - () { - expect( - () => Ack.fromJsonSchema({ - r'$ref': 'https://json-schema.org/draft/2020-12/schema', - }), - throwsA( - isA().having( - (error) => error.diagnostics.single.keyword, - 'keyword', - r'$ref', - ), - ), - ); - }, - ); - - test('strict mode reports unsupported keywords at escaped locations', () { - final document = { - 'properties': { - 'a/b': {'format': 'hostname', 'customKeyword': true}, - }, - }; - try { - Ack.fromJsonSchema(document); - fail('Expected unsupported keyword diagnostics.'); - } on JsonSchemaImportException catch (error) { - expect( - error.diagnostics.map((diagnostic) => diagnostic.pointer), - containsAll([ - '#/properties/a~1b/format', - '#/properties/a~1b/customKeyword', - ]), - ); - } - }); - - test( - 'supports multipleOf with exact decimals, tracks keywordLocation, and round-trips', - () { - final intRoundTrip = Ack.fromJsonSchema( - Ack.integer().multipleOf(5).toJsonSchema(), - ); - expect(intRoundTrip.safeParse(15).isOk, isTrue); - expect(intRoundTrip.safeParse(0).isOk, isTrue); - expect(intRoundTrip.safeParse(-10).isOk, isTrue); - final intFail = intRoundTrip.safeParse(14).getError(); - expect( - intFail, - isA() - .having((e) => e.keyword, 'keyword', 'multipleOf') - .having((e) => e.pointer, 'pointer', '#/multipleOf') - .having( - (e) => e.keywordLocation, - 'keywordLocation', - '/multipleOf', - ), - ); - - final doubleRoundTrip = Ack.fromJsonSchema( - Ack.double().multipleOf(0.01).toJsonSchema(), - ); - expect(doubleRoundTrip.safeParse(0.03).isOk, isTrue); - expect(doubleRoundTrip.safeParse(12.34).isOk, isTrue); - expect(doubleRoundTrip.safeParse(0.035).isFail, isTrue); - expect(doubleRoundTrip.safeParse(1e308).isOk, isTrue); - expect(doubleRoundTrip.safeParse(-0.0).isOk, isTrue); - expect(doubleRoundTrip.safeParse(1e-9).isFail, isTrue); - - final refSchema = Ack.fromJsonSchema({ - r'$defs': { - 'step': {'type': 'number', 'multipleOf': 0.1}, - }, - 'properties': { - 'amount': {r'$ref': r'#/$defs/step'}, - }, - }); - expect(refSchema.safeParse({'amount': 0.3}).isOk, isTrue); - final refFail = refSchema.safeParse({'amount': 0.35}).getError(); - expect( - refFail, - isA() - .having((e) => e.keyword, 'keyword', 'multipleOf') - .having((e) => e.pointer, 'pointer', r'#/$defs/step/multipleOf') - .having( - (e) => e.keywordLocation, - 'keywordLocation', - r'/properties/amount/$ref/multipleOf', - ), - ); - }, - ); - - test( - 'native and imported multipleOf both divide exactly on decimal digits', - () { - final native = Ack.double().multipleOf(0.1); - final imported = Ack.fromJsonSchema(native.toJsonSchema()); - final computed = 0.1 + 0.2; - - expect(computed, 0.30000000000000004); - expect(native.safeParse(computed).isFail, isTrue); - expect(imported.safeParse(computed).isFail, isTrue); - expect(native.safeParse(0.3).isOk, isTrue); - expect(imported.safeParse(0.3).isOk, isTrue); - expect(Ack.double().multipleOf(0.5).safeParse(1e308).isOk, isTrue); - }, - ); - - test( - 'accepts x-transformed and x-* vendor extensions and round-trips codecs', - () { - final dateJson = Ack.date().toJsonSchema(); - expect(dateJson['format'], 'date'); - expect(dateJson['x-transformed'], isTrue); - final importedDate = Ack.fromJsonSchema(dateJson); - expect(importedDate.safeParse('2024-02-29').isOk, isTrue); - expect(importedDate.safeParse('2024-02-30').isFail, isTrue); - // Imported schemas export through root-scoped definitions. - final dateDefinitions = - importedDate.toJsonSchema()['definitions'] as Map; - expect(dateDefinitions.values.first, dateJson); - - final dateTimeJson = Ack.datetime().toJsonSchema(); - expect(dateTimeJson['format'], 'date-time'); - expect(dateTimeJson['x-transformed'], isTrue); - final importedDateTime = Ack.fromJsonSchema(dateTimeJson); - expect(importedDateTime.safeParse('2024-02-29T12:00:00Z').isOk, isTrue); - expect( - importedDateTime.safeParse('2024-02-30T12:00:00Z').isFail, - isTrue, - ); - - final uriJson = Ack.uri().toJsonSchema(); - expect(uriJson['format'], 'uri'); - expect(uriJson['x-transformed'], isTrue); - final importedUri = Ack.fromJsonSchema(uriJson); - expect(importedUri.safeParse('https://example.com/path').isOk, isTrue); - expect(importedUri.safeParse('not a uri').isFail, isTrue); - - final customVendor = Ack.fromJsonSchema({ - 'type': 'string', - 'x-vendor-meta': {'tier': 'gold'}, - }); - expect(customVendor.safeParse('ok').isOk, isTrue); - final vendorDefinitions = - customVendor.toJsonSchema()['definitions'] as Map; - expect((vendorDefinitions.values.first as Map)['x-vendor-meta'], { - 'tier': 'gold', - }); - }, - ); - - test( - 'validates built-in string and numeric formats and round-trips native string formats', - () { - final emailSchema = Ack.fromJsonSchema( - Ack.string().email().toJsonSchema(), - ); - expect(emailSchema.safeParse('ada@example.com').isOk, isTrue); - final emailErr = emailSchema.safeParse('not-an-email').getError(); - expect( - emailErr, - isA() - .having((e) => e.keyword, 'keyword', 'format') - .having((e) => e.keywordLocation, 'keywordLocation', '/format'), - ); - - final uuidSchema = Ack.fromJsonSchema( - Ack.string().uuid().toJsonSchema(), - ); - expect( - uuidSchema.safeParse('550e8400-e29b-41d4-a716-446655440000').isOk, - isTrue, - ); - expect(uuidSchema.safeParse('not-a-uuid').isFail, isTrue); - - final ipv4Schema = Ack.fromJsonSchema( - Ack.string().ipv4().toJsonSchema(), - ); - expect(ipv4Schema.safeParse('192.168.1.1').isOk, isTrue); - expect(ipv4Schema.safeParse('256.0.0.1').isFail, isTrue); - expect(ipv4Schema.safeParse('01.2.3.4').isFail, isTrue); - - final ipv6Schema = Ack.fromJsonSchema( - Ack.string().ipv6().toJsonSchema(), - ); - expect(ipv6Schema.safeParse('2001:db8::1').isOk, isTrue); - expect(ipv6Schema.safeParse('::1').isOk, isTrue); - expect(ipv6Schema.safeParse('2001:db8::1::2').isFail, isTrue); - - final int32Schema = Ack.fromJsonSchema({ - 'type': 'integer', - 'format': 'int32', - }); - expect(int32Schema.safeParse(2147483647).isOk, isTrue); - expect(int32Schema.safeParse(-2147483648).isOk, isTrue); - expect(int32Schema.safeParse(2147483648).isFail, isTrue); - - final uint32Schema = Ack.fromJsonSchema({ - 'type': 'integer', - 'format': 'uint32', - }); - expect(uint32Schema.safeParse(0).isOk, isTrue); - expect(uint32Schema.safeParse(4294967295).isOk, isTrue); - expect(uint32Schema.safeParse(-1).isFail, isTrue); - expect(uint32Schema.safeParse(4294967296).isFail, isTrue); - - final int64Schema = Ack.fromJsonSchema({ - 'type': 'integer', - 'format': 'int64', - }); - expect(int64Schema.safeParse(9007199254740991).isOk, isTrue); - - final uint64Schema = Ack.fromJsonSchema({ - 'type': 'integer', - 'format': 'uint64', - }); - expect(uint64Schema.safeParse(0).isOk, isTrue); - expect(uint64Schema.safeParse(-1).isFail, isTrue); - - final floatSchema = Ack.fromJsonSchema({ - 'type': 'number', - 'format': 'float', - }); - expect(floatSchema.safeParse(1.5).isOk, isTrue); - expect(floatSchema.safeParse(1e39).isFail, isTrue); - - final doubleSchema = Ack.fromJsonSchema({ - 'type': 'number', - 'format': 'double', - }); - expect(doubleSchema.safeParse(1e300).isOk, isTrue); - - // Unknown formats still fail closed with unsupported_keyword. - for (final unsupportedFormat in ['time', 'duration', 'hostname']) { - expect( - () => Ack.fromJsonSchema({ - 'type': 'string', - 'format': unsupportedFormat, - }), - throwsA( - isA().having( - (e) => e.diagnostics.single.code, - 'code', - 'unsupported_keyword', - ), - ), - ); - } - }, - ); - - 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'}, - {'required': 'a'}, - {'minItems': -1}, - {'multipleOf': 0}, - {'multipleOf': -2}, - {'multipleOf': '2'}, - {'anyOf': []}, - {'enum': 1}, - {'items': 3}, - {'pattern': 1}, - {'pattern': '('}, - ]) { - expect( - () => Ack.fromJsonSchema(document), - throwsA(isA()), - reason: '$document', - ); - } - }); }); } diff --git a/packages/ack/test/runtime_dependencies_test.dart b/packages/ack/test/runtime_dependencies_test.dart index de763ed8..8afd947e 100644 --- a/packages/ack/test/runtime_dependencies_test.dart +++ b/packages/ack/test/runtime_dependencies_test.dart @@ -6,18 +6,24 @@ import 'dart:io'; import 'package:test/test.dart'; void main() { - test('ack depends only on meta and collection at runtime', () { - final lines = File('pubspec.yaml').readAsLinesSync(); - final start = lines.indexOf('dependencies:'); - final dependencies = [ - for (final line - in lines - .skip(start + 1) - .takeWhile((line) => line.isEmpty || line.startsWith(' '))) - if (RegExp(r'^ (\w+):').firstMatch(line) case final match?) - match.group(1), - ]; + test( + 'ack depends only on lightweight runtime packages (no json_annotation)', + () { + final lines = File('pubspec.yaml').readAsLinesSync(); + final start = lines.indexOf('dependencies:'); + final dependencies = [ + for (final line + in lines + .skip(start + 1) + .takeWhile((line) => line.isEmpty || line.startsWith(' '))) + if (RegExp(r'^ (\w+):').firstMatch(line) case final match?) + match.group(1), + ]; - expect(dependencies, unorderedEquals(['meta', 'collection'])); - }); + expect( + dependencies, + unorderedEquals(['meta', 'collection', 'punycoder', 'unorm_dart']), + ); + }, + ); } diff --git a/packages/ack/test/schema_model/ack_schema_model_builder_test.dart b/packages/ack/test/schema_model/ack_schema_model_builder_test.dart index ccfa5d9b..84cdb019 100644 --- a/packages/ack/test/schema_model/ack_schema_model_builder_test.dart +++ b/packages/ack/test/schema_model/ack_schema_model_builder_test.dart @@ -162,55 +162,77 @@ void main() { ); }); - test('uses existing composition models for imported JSON Schema', () { - final schema = Ack.fromJsonSchema({'type': 'string', 'minLength': 2}); + test( + 'uses a resource-preserving model for imported JSON Schema by default', + () { + final schema = Ack.fromJsonSchema({'type': 'string', 'minLength': 2}); - final model = schema.toSchemaModel(); + final model = schema.toSchemaModel(); - expect(model, isA()); - final allOf = model as AckAllOfSchemaModel; - expect(allOf.schemas, hasLength(1)); - expect(allOf.schemas.single, isA()); - expect(model.toJsonSchema(), schema.toJsonSchema()); - }); + expect(model, isA()); + expect(model.toJsonSchema()['type'], 'string'); + expect(model.toJsonSchema(), schema.toJsonSchema()); + }, + ); - test('rejects an imported definition reused as a lazy target', () { - final imported = Ack.fromJsonSchema(true).nullable(value: false); - final schema = Ack.object({ - 'a': imported, - 'b': Ack.lazy('_ack_import_0_0', () => imported), - }); + test( + 'uses existing composition models for Draft-7 lowered imported JSON Schema', + () { + final schema = Ack.fromJsonSchema({'type': 'string', 'minLength': 2}); - expect( - schema.toJsonSchema, - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('collides with an imported definition'), + final model = schema.toSchemaModelDraft7(); + + expect(model, isA()); + final allOf = model as AckAllOfSchemaModel; + expect(allOf.schemas, hasLength(1)); + expect(allOf.schemas.single, isA()); + expect(model.toJsonSchema(), schema.toJsonSchemaDraft7()); + }, + ); + + test( + 'rejects an imported definition reused as a lazy target in Draft-7 lowering', + () { + final imported = Ack.fromJsonSchema(true).nullable(value: false); + final schema = Ack.object({ + 'a': imported, + 'b': Ack.lazy('_ack_import_0_0', () => imported), + }); + + expect( + schema.toJsonSchemaDraft7, + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('collides with an imported definition'), + ), ), - ), - ); - }); + ); + }, + ); - test('rejects an import generated name occupied by an earlier lazy', () { - final imported = Ack.fromJsonSchema(true); - final schema = Ack.object({ - 'a': Ack.lazy('_ack_import_0_0', Ack.string), - 'b': imported, - }); + test( + 'rejects an import generated name occupied by an earlier lazy in Draft-7 lowering', + () { + final imported = Ack.fromJsonSchema(true); + final schema = Ack.object({ + 'a': Ack.lazy('_ack_import_0_0', Ack.string), + 'b': imported, + }); - expect( - schema.toJsonSchema, - throwsA( - isA().having( - (error) => error.message, - 'message', - contains('Imported definition collides with'), + expect( + schema.toJsonSchemaDraft7, + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('Imported definition collides with'), + ), ), - ), - ); - }); + ); + }, + ); test('rejects nullable list item schemas at construction', () { expect( diff --git a/packages/ack/test/schemas/effective_nullability_test.dart b/packages/ack/test/schemas/effective_nullability_test.dart index ab0fcf57..3e9864c9 100644 --- a/packages/ack/test/schemas/effective_nullability_test.dart +++ b/packages/ack/test/schemas/effective_nullability_test.dart @@ -6,6 +6,31 @@ AnyOfSchema _nullableUnion() => Ack.anyOf([Ack.string().nullable(), Ack.integer()]); void main() { + group('nullable wrapper composition', () { + test('keeps optional fields optional and retains their description', () { + final field = Ack.string().optional().describe('Display name').nullable(); + final schema = Ack.object({'name': field}); + + expect(field.isOptional, isTrue); + expect(field.description, 'Display name'); + expect(schema.safeParse({}).isOk, isTrue); + expect(schema.safeEncode({}).isOk, isTrue); + expect(schema.safeParse({'name': null}).isOk, isTrue); + }); + + test('enforces refinements added after nullable on parse and encode', () { + final schema = Ack.string().nullable().refine( + (value) => value != 'blocked', + message: 'Blocked name', + ); + + expect(schema.safeParse('blocked').isFail, isTrue); + expect(schema.safeEncode('blocked').isFail, isTrue); + expect(schema.safeParse('allowed').isOk, isTrue); + expect(schema.safeParse(null).isOk, isTrue); + }); + }); + group('effective nullability — object fields', () { test('required nullable-union field parses and encodes null', () { final schema = Ack.object({'v': _nullableUnion()}); diff --git a/packages/ack_json_schema_builder/CHANGELOG.md b/packages/ack_json_schema_builder/CHANGELOG.md index f48cf04d..21ad1303 100644 --- a/packages/ack_json_schema_builder/CHANGELOG.md +++ b/packages/ack_json_schema_builder/CHANGELOG.md @@ -1,3 +1,10 @@ +## Unreleased + +### Changed + +* Imports inherit the expanded Draft 2020-12 runtime validator. Formats are + annotations by default; the bridge's public API is unchanged. + ## 1.7.0-beta.6 ### Changed diff --git a/packages/ack_json_schema_builder/README.md b/packages/ack_json_schema_builder/README.md index 7b2bc10c..7f787fb6 100644 --- a/packages/ack_json_schema_builder/README.md +++ b/packages/ack_json_schema_builder/README.md @@ -33,22 +33,22 @@ the source keyword and location. `toAckSchema()` returns the executable validator. Exports preserve supported validation behavior, not textual round-trip identity. -The subset includes objects, arrays, primitives, enum/const, numeric bounds, -length constraints, `anyOf`/`allOf`/exclusive `oneOf`/`not`, and recursive -references. Formats, patterns, multiples, dynamic references, and meta-schema -validation are not supported. Exports contain only enforced assertions. -The published A2UI basic catalog also requires unsupported unevaluated-property -checks and conditionals, so strict import rejects it. Full A2UI support and MCP +The bridge inherits ACK's required draft 2020-12 validation support, including +patterns, multiples, dynamic references, conditionals, unevaluated keywords, +and offline standard meta-schemas. Formats are annotations by default. To +assert supported formats before the bridge exposes an assertion option, use +`Ack.fromJsonSchema(..., assertFormats: true)` directly. The bridge's default +export remains the stable Draft-7 representation. Full A2UI support and MCP registration compatibility remain separate work. See the [import guide](https://concepta.dev/documentation/ack/guides/json-schema-integration) -for the complete support matrix and reference/diagnostic behavior. +for coverage and reference/diagnostic behavior. ## Installation ```yaml dependencies: - ack: ^1.7.0-beta.6 - ack_json_schema_builder: ^1.7.0-beta.6 + ack: ^1.6.2 + ack_json_schema_builder: ^1.6.2 json_schema_builder: ^0.1.3 ``` diff --git a/packages/ack_json_schema_builder/test/from_json_schema_builder_test.dart b/packages/ack_json_schema_builder/test/from_json_schema_builder_test.dart index e4aff202..0d225647 100644 --- a/packages/ack_json_schema_builder/test/from_json_schema_builder_test.dart +++ b/packages/ack_json_schema_builder/test/from_json_schema_builder_test.dart @@ -19,24 +19,10 @@ void main() { expect(strict.safeParse({}).isFail, isTrue); }); - test('builder bridge retains strictness and diagnostics', () { - final model = jsb.Schema.fromMap({'format': 'hostname'}); - expect( - () => model.toAckSchema(), - throwsA( - isA().having( - (error) => error.diagnostics.single.keyword, - 'keyword', - 'format', - ), - ), - ); - }); - - test('builder bridge asserts supported date-time on import', () { - final model = jsb.Schema.fromMap({'type': 'string', 'format': 'date-time'}); + test('builder bridge treats formats as annotations by default', () { + final model = jsb.Schema.fromMap({'type': 'string', 'format': 'email'}); final schema = model.toAckSchema(); - expect(schema.safeParse('2024-02-29T01:02:03Z').isOk, isTrue); - expect(schema.safeParse('2024-02-30T01:02:03Z').isFail, isTrue); + expect(schema.safeParse('not an email').isOk, isTrue); + expect(schema.safeParse(42).isFail, isTrue); }); }