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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion coverage_baseline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ packages/a2ui_core: 76.54
packages/genai_primitives: 100.00
packages/genui: 79.66
packages/genui_a2a: 91.37
packages/json_schema_builder: 79.09
packages/json_schema_builder: 81.93
tool/e2e: 100.00
tool/fix_copyright: 89.83
tool/release: 78.01
Expand Down
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