Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/json_schema_builder/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# [json_schema_builder](https://pub.dev/packages/json_schema_builder) Change Log

## 0.1.7

- **Feature**: Add `Schema.validateSync`, a synchronous validation entry point
for schemas whose references all resolve without fetching. Validation itself
is now a synchronous core, and the existing `Schema.validate` is a thin
asynchronous wrapper around it that fetches remote references into the
`SchemaRegistry` as they are needed. Behavior of `validate` is unchanged.
- **Feature**: Add `SchemaRegistry.resolveSync` and `SchemaRegistry.fetch`,
which split reference resolution from fetching.
- **Feature**: Export `SchemaFetchException` and the new
`SchemaResolutionRequiredException`, which `validateSync` throws when a
reference can only be resolved by fetching.

## 0.1.6

- **Feature**: Export `SchemaRegistry` to support managing schema references during component validation.
Expand Down
30 changes: 30 additions & 0 deletions packages/json_schema_builder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,33 @@ Future<void> main() async {
- List contains duplicate items at path #root["roles"]
- Additional property "extraField" is not allowed. at path #root["extraField"]
```

### Synchronous Validation

`validate` is asynchronous only because a `$ref` may point at a schema that has
to be fetched. If your schema has no such references — because they are inlined,
or because you registered every referenced schema up front — use `validateSync`
instead and skip the `Future`:

```dart
final errors = userProfileSchema.validateSync(validUser);
```

`validateSync` performs no I/O. If validation reaches a reference whose target
would have to be fetched, it throws a `SchemaResolutionRequiredException` naming
that target instead of skipping the reference, so a missing fetch can never turn
into a passing validation.

To validate synchronously against a schema that does have remote references,
give both calls the same `SchemaRegistry`. The asynchronous call fetches what it
needs into the registry, and every later validation can be synchronous:

```dart
final registry = SchemaRegistry();
await schema.validate(firstValue, schemaRegistry: registry);
final errors = schema.validateSync(secondValue, schemaRegistry: registry);
```

Note that a schema declaring a `$schema` meta-schema needs that meta-schema
resolved too, so pre-register it (or fetch it with one `validate` call) before
validating synchronously.
1 change: 1 addition & 0 deletions packages/json_schema_builder/lib/json_schema_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

export 'src/exceptions.dart';
export 'src/json_type.dart';
export 'src/schema/boolean_schema.dart';
export 'src/schema/integer_schema.dart';
Expand Down
24 changes: 24 additions & 0 deletions packages/json_schema_builder/lib/src/exceptions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,27 @@ class SchemaFetchException implements Exception {
return message;
}
}

/// Thrown by the synchronous validation path when a reference can only be
/// resolved by fetching a schema that is not in the `SchemaRegistry` yet.
///
/// Synchronous validation never performs I/O, so it cannot fetch the schema
/// itself. Rather than treating the unresolved subschema as unconstrained,
/// which would silently turn a missing fetch into a passing validation, it
/// throws this exception.
///
/// To fix it, either add the schema at [uri] to the registry before
/// validating, or use the asynchronous `validate` method, which fetches
/// remote schemas as it needs them.
class SchemaResolutionRequiredException implements Exception {
/// The URI of the schema that would have to be fetched, without any fragment.
final Uri uri;

SchemaResolutionRequiredException(this.uri);

@override
String toString() =>
'Synchronous validation requires the schema at $uri, which is not '
'registered. Add it to the SchemaRegistry, or use the asynchronous '
'Schema.validate to fetch it.';
}
51 changes: 38 additions & 13 deletions packages/json_schema_builder/lib/src/schema_registry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,23 +43,48 @@ class SchemaRegistry {
///
/// This method can also resolve fragments and JSON pointers within a schema.
Future<Schema?> resolve(Uri uri) async {
final Schema? schema = await fetch(uri);
if (schema == null) return null;
return _getSchemaFromFragment(uri, schema);
}

/// Resolves a schema from the given [uri] without performing any I/O.
///
/// This behaves like [resolve], except that it only looks at the schemas this
/// registry already holds: those added with [addSchema], and those a previous
/// [resolve] or [fetch] call brought in.
///
/// Throws a [SchemaResolutionRequiredException] if resolving [uri] would
/// require fetching a schema that this registry does not hold.
Schema? resolveSync(Uri uri) {
final Uri uriWithoutFragment = uri.removeFragment();
if (_schemas.containsKey(uriWithoutFragment)) {
return _getSchemaFromFragment(uri, _schemas[uriWithoutFragment]!);
final Schema? schema = _schemas[uriWithoutFragment];
if (schema == null) {
throw SchemaResolutionRequiredException(uriWithoutFragment);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a comment: I was hoping that we could check and see if the URI was a file URI and then resolve it sync here, but I guess if we're going to be using this on the web ever, then that will mean that dart:io isn't available, so sync file reading also wouldn't be.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true

}
return _getSchemaFromFragment(uri, schema);
}

try {
final Schema? schema = await _schemaCache.get(uriWithoutFragment);
if (schema == null) {
return null;
}
_schemas[uriWithoutFragment] = schema;
_registerIds(schema, uriWithoutFragment);
/// Fetches the schema resource that [uri] points into, ignoring any fragment,
/// and adds it to this registry.
///
/// Returns the schema, or `null` if there is none. If the registry already
/// holds it, it is returned without any I/O. Throws a [SchemaFetchException]
/// if the fetch fails.
///
/// This is the asynchronous counterpart to [resolveSync]: it performs the
/// I/O that [resolveSync] refuses to perform, so that a subsequent
/// [resolveSync] of the same URI can answer from memory.
Future<Schema?> fetch(Uri uri) async {
final Uri uriWithoutFragment = uri.removeFragment();
final Schema? registered = _schemas[uriWithoutFragment];
if (registered != null) return registered;

return _getSchemaFromFragment(uri, schema);
} on SchemaFetchException {
rethrow;
}
final Schema? schema = await _schemaCache.get(uriWithoutFragment);
if (schema == null) return null;
_schemas[uriWithoutFragment] = schema;
_registerIds(schema, uriWithoutFragment);
return schema;
}

/// Gets the URI for a given schema, if it has been registered.
Expand Down
Loading
Loading