auto validate tool arguments, improve validation messages (#200)
Marking this as closing https://github.com/dart-lang/ai/issues/197 - it should significantly help the LLM figure out what it did wrong. The error would now be something like "Value `<root>` is not of type `List<dynamic>` at path #root["roots"]".
I made a fair number of changes to the validation errors here, namely only emitting errors for the leaf node where the error actually happened and not the entire path up. I don't think that extra info was helpful.
cc @kenzieschmoll
diff --git a/pkgs/dart_mcp/CHANGELOG.md b/pkgs/dart_mcp/CHANGELOG.md
index 700381a..008ac99 100644
--- a/pkgs/dart_mcp/CHANGELOG.md
+++ b/pkgs/dart_mcp/CHANGELOG.md
@@ -1,8 +1,18 @@
-## 0.2.3-wip
+## 0.3.0-wip
- Added error checking to required fields of all `Request` subclasses so that
they will throw helpful errors when accessed and not set.
- Added enum support to Schema.
+- Add more detail to type validation errors.
+- Remove some duplicate validation errors, errors are only reported for the
+ leaf nodes and not all the way up the tree.
+ - Deprecated a few validation error types as a part of this, including
+ `propertyNamesInvalid`, `propertyValueInvalid`, `itemInvalid` and
+ `prefixItemInvalid`.
+- Added a `custom` validation error type.
+- Auto-validate schemas for all tools by default. This can be disabled by
+ passing `validateArguments: false` to `registerTool`.
+ - This is breaking since this method is overridden by the Dart MCP server.
- Updates to the latest MCP spec, [2025-06-08](https://modelcontextprotocol.io/specification/2025-06-18/changelog)
- Adds support for Elicitations to allow the server to ask the user questions.
- Adds `ResourceLink` as a tool return content type.
diff --git a/pkgs/dart_mcp/lib/src/api/tools.dart b/pkgs/dart_mcp/lib/src/api/tools.dart
index 8ea339b..85a2c09 100644
--- a/pkgs/dart_mcp/lib/src/api/tools.dart
+++ b/pkgs/dart_mcp/lib/src/api/tools.dart
@@ -245,6 +245,9 @@
/// Enum representing the types of validation failures when checking data
/// against a schema.
enum ValidationErrorType {
+ // For custom validation.
+ custom,
+
// General
typeMismatch,
@@ -259,7 +262,15 @@
additionalPropertyNotAllowed,
minPropertiesNotMet,
maxPropertiesExceeded,
+ @Deprecated(
+ 'These events are no longer emitted, just emit a single error for the '
+ 'key itself',
+ )
propertyNamesInvalid,
+ @Deprecated(
+ 'These events are no longer emitted, just emit a single error for the '
+ 'property itself',
+ )
propertyValueInvalid,
patternPropertyValueInvalid,
unevaluatedPropertyNotAllowed,
@@ -268,7 +279,15 @@
minItemsNotMet,
maxItemsExceeded,
uniqueItemsViolated,
+ @Deprecated(
+ 'These events are no longer emitted, just emit a single error for the '
+ 'item itself',
+ )
itemInvalid,
+ @Deprecated(
+ 'These events are no longer emitted, just emit a single error for the '
+ 'prefix item itself',
+ )
prefixItemInvalid,
unevaluatedItemNotAllowed,
@@ -293,29 +312,38 @@
extension type ValidationError.fromMap(Map<String, Object?> _value) {
factory ValidationError(
ValidationErrorType error, {
- List<String>? path,
+ required List<String> path,
String? details,
}) => ValidationError.fromMap({
'error': error.name,
- if (path != null) 'path': path.toList(),
+ 'path': path.toList(),
if (details != null) 'details': details,
});
- /// The type of validation error that occurred.
- ValidationErrorType? get error => ValidationErrorType.values.firstWhereOrNull(
- (t) => t.name == _value['error'],
+ factory ValidationError.typeMismatch({
+ required List<String> path,
+ required Type expectedType,
+ required Object? actualValue,
+ }) => ValidationError(
+ ValidationErrorType.typeMismatch,
+ path: path,
+ details: 'Value `$actualValue` is not of type `$expectedType`',
);
+ /// The type of validation error that occurred.
+ ValidationErrorType get error =>
+ ValidationErrorType.values.firstWhere((t) => t.name == _value['error']);
+
/// The path to the object that had the error.
- List<String>? get path => (_value['path'] as List?)?.cast<String>();
+ List<String> get path => (_value['path'] as List).cast<String>();
/// Additional details about the error (optional).
String? get details => _value['details'] as String?;
String toErrorString() {
- return '${error!.name} in object at '
- '${path!.map((p) => '[$p]').join('')}'
- '${details != null ? ' - $details' : ''}';
+ return '${details != null ? '$details' : error.name} at path '
+ '#root${path.map((p) => '["$p"]').join('')}'
+ '';
}
}
@@ -479,9 +507,10 @@
if (data is! bool) {
isValid = false;
accumulatedFailures.add(
- ValidationError(
- ValidationErrorType.typeMismatch,
+ ValidationError.typeMismatch(
path: currentPath,
+ expectedType: bool,
+ actualValue: data,
),
);
}
@@ -489,9 +518,10 @@
if (data != null) {
isValid = false;
accumulatedFailures.add(
- ValidationError(
- ValidationErrorType.typeMismatch,
+ ValidationError.typeMismatch(
path: currentPath,
+ expectedType: Null,
+ actualValue: data,
),
);
}
@@ -512,9 +542,9 @@
// Validate data against the non-combinator keywords of the current schema
// ('this').
- if (!_performDirectValidation(data, currentPath, accumulatedFailures)) {
- isValid = false;
- }
+ isValid =
+ _performDirectValidation(data, currentPath, accumulatedFailures) &&
+ isValid;
// Handle combinator keywords. Create the "base schema" from 'this' schema,
// excluding combinator keywords. This base schema's constraints are
@@ -544,19 +574,16 @@
if (allOf case final allOfList?) {
var allSubSchemasAreValid = true;
- final allOfDetailedSubFailures = <ValidationError>[];
for (final subSchemaMember in allOfList) {
final effectiveSubSchema = mergeWithBase(subSchemaMember);
- final currentSubSchemaFailures = _createHashSet();
- if (!effectiveSubSchema._validateSchema(
- data,
- currentPath,
- currentSubSchemaFailures,
- )) {
- allSubSchemasAreValid = false;
- allOfDetailedSubFailures.addAll(currentSubSchemaFailures);
- }
+ allSubSchemasAreValid =
+ effectiveSubSchema._validateSchema(
+ data,
+ currentPath,
+ accumulatedFailures,
+ ) &&
+ allSubSchemasAreValid;
}
// `allOf` fails if any effective sub-schema (Base AND SubMember) failed.
if (!allSubSchemasAreValid) {
@@ -564,7 +591,6 @@
accumulatedFailures.add(
ValidationError(ValidationErrorType.allOfNotMet, path: currentPath),
);
- accumulatedFailures.addAll(allOfDetailedSubFailures);
}
}
if (anyOf case final anyOfList?) {
@@ -584,7 +610,11 @@
if (!oneSubSchemaPassed) {
isValid = false;
accumulatedFailures.add(
- ValidationError(ValidationErrorType.anyOfNotMet, path: currentPath),
+ ValidationError(
+ ValidationErrorType.anyOfNotMet,
+ path: currentPath,
+ details: 'No sub-schema passed validation for $data',
+ ),
);
}
}
@@ -603,30 +633,35 @@
if (matchingSubSchemaCount != 1) {
isValid = false;
accumulatedFailures.add(
- ValidationError(ValidationErrorType.oneOfNotMet, path: currentPath),
+ ValidationError(
+ ValidationErrorType.oneOfNotMet,
+ path: currentPath,
+ details:
+ 'Exactly one sub-schema must match $data but '
+ '$matchingSubSchemaCount did',
+ ),
);
}
}
if (not case final notList?) {
- final notConditionViolatedBySubSchema = notList.any((subSchemaInNot) {
+ for (final subSchemaInNot in notList) {
final effectiveSubSchemaForNot = mergeWithBase(subSchemaInNot);
// 'not' is violated if data *validates* against the (Base AND
// NotSubSchema).
- return effectiveSubSchemaForNot._validateSchema(
+ if (effectiveSubSchemaForNot._validateSchema(
data,
currentPath,
_createHashSet(),
- );
- });
-
- if (notConditionViolatedBySubSchema) {
- isValid = false;
- accumulatedFailures.add(
- ValidationError(
- ValidationErrorType.notConditionViolated,
- path: currentPath,
- ),
- );
+ )) {
+ isValid = false;
+ accumulatedFailures.add(
+ ValidationError(
+ ValidationErrorType.notConditionViolated,
+ path: currentPath,
+ details: '$data matched the schema $subSchemaInNot',
+ ),
+ );
+ }
}
}
@@ -872,7 +907,11 @@
) {
if (data is! Map<String, Object?>) {
accumulatedFailures.add(
- ValidationError(ValidationErrorType.typeMismatch, path: currentPath),
+ ValidationError.typeMismatch(
+ path: currentPath,
+ expectedType: Map<String, Object?>,
+ actualValue: data,
+ ),
);
return false;
}
@@ -887,7 +926,7 @@
path: currentPath,
details:
'There should be at least $minProperties '
- 'properties. Only ${data.keys.length} were found.',
+ 'properties. Only ${data.keys.length} were found',
),
);
}
@@ -900,7 +939,7 @@
path: currentPath,
details:
'Exceeded maxProperties limit of $maxProperties '
- '(${data.keys.length}).',
+ '(${data.keys.length})',
),
);
}
@@ -912,7 +951,7 @@
ValidationError(
ValidationErrorType.requiredPropertyMissing,
path: currentPath,
- details: 'Required property "$reqProp" is missing.',
+ details: 'Required property "$reqProp" is missing',
),
);
}
@@ -928,21 +967,13 @@
if (data.containsKey(entry.key)) {
currentPath.add(entry.key);
evaluatedKeys.add(entry.key);
- final propertySpecificFailures = _createHashSet();
- if (!entry.value._validateSchema(
- data[entry.key],
- currentPath,
- propertySpecificFailures,
- )) {
- isValid = false;
- accumulatedFailures.add(
- ValidationError(
- ValidationErrorType.propertyValueInvalid,
- path: currentPath,
- ),
- );
- accumulatedFailures.addAll(propertySpecificFailures);
- }
+ isValid =
+ entry.value._validateSchema(
+ data[entry.key],
+ currentPath,
+ accumulatedFailures,
+ ) &&
+ isValid;
currentPath.removeLast();
}
}
@@ -959,21 +990,13 @@
if (pattern.hasMatch(dataKey)) {
currentPath.add(dataKey);
evaluatedKeys.add(dataKey);
- final patternPropertySpecificFailures = _createHashSet();
- if (!entry.value._validateSchema(
- data[dataKey],
- currentPath,
- patternPropertySpecificFailures,
- )) {
- isValid = false;
- accumulatedFailures.add(
- ValidationError(
- ValidationErrorType.patternPropertyValueInvalid,
- path: currentPath,
- ),
- );
- accumulatedFailures.addAll(patternPropertySpecificFailures);
- }
+ isValid =
+ entry.value._validateSchema(
+ data[dataKey],
+ currentPath,
+ accumulatedFailures,
+ ) &&
+ isValid;
currentPath.removeLast();
}
}
@@ -987,21 +1010,13 @@
if (propertyNames case final propNamesSchema?) {
for (final key in data.keys) {
currentPath.add(key);
- final propertyNameSpecificFailures = _createHashSet();
- if (!propNamesSchema._validateSchema(
- key,
- currentPath,
- propertyNameSpecificFailures,
- )) {
- isValid = false;
- accumulatedFailures.addAll(propertyNameSpecificFailures);
- accumulatedFailures.add(
- ValidationError(
- ValidationErrorType.propertyNamesInvalid,
- path: currentPath,
- ),
- );
- }
+ isValid =
+ propNamesSchema._validateSchema(
+ key,
+ currentPath,
+ accumulatedFailures,
+ ) &&
+ isValid;
currentPath.removeLast();
}
}
@@ -1013,32 +1028,26 @@
for (final dataKey in data.keys) {
if (evaluatedKeys.contains(dataKey)) continue;
- var isAdditionalPropertyAllowed = true;
if (additionalProperties != null) {
final ap = additionalProperties;
currentPath.add(dataKey);
if (ap is bool && !ap) {
- isAdditionalPropertyAllowed = false;
- } else if (ap is Schema) {
- final additionalPropSchemaFailures = _createHashSet();
- if (!ap._validateSchema(
- data[dataKey],
- currentPath,
- additionalPropSchemaFailures,
- )) {
- isAdditionalPropertyAllowed = false;
- // Add details why it failed
- accumulatedFailures.addAll(additionalPropSchemaFailures);
- }
- }
- if (!isAdditionalPropertyAllowed) {
isValid = false;
accumulatedFailures.add(
ValidationError(
ValidationErrorType.additionalPropertyNotAllowed,
path: currentPath,
+ details: 'Additional property "$dataKey" is not allowed',
),
);
+ } else if (ap is Schema) {
+ isValid =
+ ap._validateSchema(
+ data[dataKey],
+ currentPath,
+ accumulatedFailures,
+ ) &&
+ isValid;
}
currentPath.removeLast();
} else if (unevaluatedProperties == false) {
@@ -1049,6 +1058,7 @@
ValidationError(
ValidationErrorType.unevaluatedPropertyNotAllowed,
path: currentPath,
+ details: 'Unevaluated property "$dataKey" is not allowed',
),
);
currentPath.removeLast();
@@ -1092,7 +1102,11 @@
) {
if (data is! String) {
accumulatedFailures.add(
- ValidationError(ValidationErrorType.typeMismatch, path: currentPath),
+ ValidationError.typeMismatch(
+ path: currentPath,
+ expectedType: String,
+ actualValue: data,
+ ),
);
return false;
}
@@ -1103,7 +1117,7 @@
ValidationError(
ValidationErrorType.minLengthNotMet,
path: currentPath,
- details: 'String "$data" is not at least $minLen characters long.',
+ details: 'String "$data" is not at least $minLen characters long',
),
);
}
@@ -1113,7 +1127,7 @@
ValidationError(
ValidationErrorType.maxLengthExceeded,
path: currentPath,
- details: 'String "$data" is more than $maxLen characters long.',
+ details: 'String "$data" is more than $maxLen characters long',
),
);
}
@@ -1124,7 +1138,7 @@
ValidationError(
ValidationErrorType.patternMismatch,
path: currentPath,
- details: 'String "$data" doesn\'t match the pattern "$dataPattern".',
+ details: 'String "$data" doesn\'t match the pattern "$dataPattern"',
),
);
}
@@ -1172,7 +1186,11 @@
) {
if (data is! String) {
accumulatedFailures.add(
- ValidationError(ValidationErrorType.typeMismatch, path: currentPath),
+ ValidationError.typeMismatch(
+ path: currentPath,
+ expectedType: String,
+ actualValue: data,
+ ),
);
return false;
}
@@ -1236,7 +1254,11 @@
) {
if (data is! num) {
accumulatedFailures.add(
- ValidationError(ValidationErrorType.typeMismatch, path: currentPath),
+ ValidationError.typeMismatch(
+ path: currentPath,
+ expectedType: num,
+ actualValue: data,
+ ),
);
return false;
}
@@ -1248,7 +1270,7 @@
ValidationError(
ValidationErrorType.minimumNotMet,
path: currentPath,
- details: 'Value $data is not at least $min.',
+ details: 'Value $data is not at least $min',
),
);
}
@@ -1258,7 +1280,7 @@
ValidationError(
ValidationErrorType.maximumExceeded,
path: currentPath,
- details: 'Value $data is larger than $max.',
+ details: 'Value $data is larger than $max',
),
);
}
@@ -1269,7 +1291,7 @@
ValidationErrorType.exclusiveMinimumNotMet,
path: currentPath,
- details: 'Value $data is not greater than $exclusiveMin.',
+ details: 'Value $data is not greater than $exclusiveMin',
),
);
}
@@ -1279,7 +1301,7 @@
ValidationError(
ValidationErrorType.exclusiveMaximumExceeded,
path: currentPath,
- details: 'Value $data is not less than $exclusiveMax.',
+ details: 'Value $data is not less than $exclusiveMax',
),
);
}
@@ -1291,7 +1313,7 @@
ValidationError(
ValidationErrorType.multipleOfInvalid,
path: currentPath,
- details: 'Value $data is not a multiple of $multipleOf.',
+ details: 'Value $data is not a multiple of $multipleOf',
),
);
}
@@ -1344,12 +1366,10 @@
) {
if (data == null || (data is! int && data is! num)) {
accumulatedFailures.add(
- ValidationError(
- ValidationErrorType.typeMismatch,
+ ValidationError.typeMismatch(
path: currentPath,
- details:
- 'Value $data has the type ${data.runtimeType}, which is not '
- 'an integer.',
+ expectedType: int,
+ actualValue: data,
),
);
return false;
@@ -1361,7 +1381,7 @@
ValidationError(
ValidationErrorType.typeMismatch,
path: currentPath,
- details: 'Value $data is a number, but is not an integer.',
+ details: 'Value $data is a number, but is not an integer',
),
);
return false;
@@ -1377,7 +1397,7 @@
ValidationError(
ValidationErrorType.minimumNotMet,
path: currentPath,
- details: 'Value $data is less than the minimum of $min.',
+ details: 'Value $data is less than the minimum of $min',
),
);
}
@@ -1387,7 +1407,7 @@
ValidationError(
ValidationErrorType.maximumExceeded,
path: currentPath,
- details: 'Value $data is more than the maximum of $max.',
+ details: 'Value $data is more than the maximum of $max',
),
);
}
@@ -1397,7 +1417,7 @@
ValidationError(
ValidationErrorType.exclusiveMinimumNotMet,
path: currentPath,
- details: 'Value $data is not greater than $exclusiveMin.',
+ details: 'Value $data is not greater than $exclusiveMin',
),
);
}
@@ -1407,7 +1427,7 @@
ValidationError(
ValidationErrorType.exclusiveMaximumExceeded,
path: currentPath,
- details: 'Value $data is not less than $exclusiveMax.',
+ details: 'Value $data is not less than $exclusiveMax',
),
);
}
@@ -1418,7 +1438,7 @@
ValidationError(
ValidationErrorType.multipleOfInvalid,
path: currentPath,
- details: 'Value $data is not a multiple of $multOf.',
+ details: 'Value $data is not a multiple of $multOf',
),
);
}
@@ -1584,7 +1604,11 @@
) {
if (data is! List<dynamic>) {
accumulatedFailures.add(
- ValidationError(ValidationErrorType.typeMismatch, path: currentPath),
+ ValidationError.typeMismatch(
+ path: currentPath,
+ expectedType: List<dynamic>,
+ actualValue: data,
+ ),
);
return false;
}
@@ -1599,7 +1623,7 @@
path: currentPath,
details:
'List has ${data.length} items, but must have at least '
- '$min.',
+ '$min',
),
);
}
@@ -1612,7 +1636,7 @@
path: currentPath,
details:
'List has ${data.length} items, but must have less than '
- '$max.',
+ '$max',
),
);
}
@@ -1632,7 +1656,7 @@
ValidationError(
ValidationErrorType.uniqueItemsViolated,
path: currentPath,
- details: 'List contains duplicate items: ${duplicates.join(', ')}.',
+ details: 'List contains duplicate items: ${duplicates.join(', ')}',
),
);
}
@@ -1642,21 +1666,13 @@
for (var i = 0; i < pItems.length && i < data.length; i++) {
evaluatedItems[i] = true;
currentPath.add(i.toString());
- final prefixItemSpecificFailures = _createHashSet();
- if (!pItems[i]._validateSchema(
- data[i],
- currentPath,
- prefixItemSpecificFailures,
- )) {
- isValid = false;
- accumulatedFailures.add(
- ValidationError(
- ValidationErrorType.prefixItemInvalid,
- path: currentPath,
- ),
- );
- accumulatedFailures.addAll(prefixItemSpecificFailures);
- }
+ isValid =
+ pItems[i]._validateSchema(
+ data[i],
+ currentPath,
+ accumulatedFailures,
+ ) &&
+ isValid;
currentPath.removeLast();
}
}
@@ -1665,18 +1681,13 @@
for (var i = startIndex; i < data.length; i++) {
evaluatedItems[i] = true;
currentPath.add(i.toString());
- final itemSpecificFailures = _createHashSet();
- if (!itemSchema._validateSchema(
- data[i],
- currentPath,
- itemSpecificFailures,
- )) {
- isValid = false;
- accumulatedFailures.add(
- ValidationError(ValidationErrorType.itemInvalid, path: currentPath),
- );
- accumulatedFailures.addAll(itemSpecificFailures);
- }
+ isValid =
+ itemSchema._validateSchema(
+ data[i],
+ currentPath,
+ accumulatedFailures,
+ ) &&
+ isValid;
currentPath.removeLast();
}
}
@@ -1713,11 +1724,7 @@
a.error == b.error;
},
hashCode: (ValidationError error) {
- return Object.hashAll([
- ...error.path ?? const [],
- error.details,
- error.error,
- ]);
+ return Object.hashAll([...error.path, error.details, error.error]);
},
);
}
diff --git a/pkgs/dart_mcp/lib/src/server/tools_support.dart b/pkgs/dart_mcp/lib/src/server/tools_support.dart
index e255019..6521b86 100644
--- a/pkgs/dart_mcp/lib/src/server/tools_support.dart
+++ b/pkgs/dart_mcp/lib/src/server/tools_support.dart
@@ -43,17 +43,38 @@
///
/// Throws a [StateError] if there is already a [Tool] registered with the
/// same name.
+ ///
+ /// If [validateArguments] is true, then request arguments are automatically
+ /// validated against the [tool]s input schema.
void registerTool(
Tool tool,
- FutureOr<CallToolResult> Function(CallToolRequest) impl,
- ) {
+ FutureOr<CallToolResult> Function(CallToolRequest) impl, {
+ bool validateArguments = true,
+ }) {
if (_registeredTools.containsKey(tool.name)) {
throw StateError(
'Failed to register tool ${tool.name}, it is already registered',
);
}
_registeredTools[tool.name] = tool;
- _registeredToolImpls[tool.name] = impl;
+ _registeredToolImpls[tool.name] =
+ validateArguments
+ ? (request) {
+ final errors = tool.inputSchema.validate(
+ request.arguments ?? const <String, Object?>{},
+ );
+ if (errors.isNotEmpty) {
+ return CallToolResult(
+ content: [
+ for (final error in errors)
+ Content.text(text: error.toErrorString()),
+ ],
+ isError: true,
+ );
+ }
+ return impl(request);
+ }
+ : impl;
if (ready) {
_notifyToolListChanged();
diff --git a/pkgs/dart_mcp/pubspec.yaml b/pkgs/dart_mcp/pubspec.yaml
index 3a98683..0e48b2a 100644
--- a/pkgs/dart_mcp/pubspec.yaml
+++ b/pkgs/dart_mcp/pubspec.yaml
@@ -1,5 +1,5 @@
name: dart_mcp
-version: 0.2.3-wip
+version: 0.3.0-wip
description: A package for making MCP servers and clients.
repository: https://github.com/dart-lang/ai/tree/main/pkgs/dart_mcp
issue_tracker: https://github.com/dart-lang/ai/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Adart_mcp
diff --git a/pkgs/dart_mcp/test/api/tools_test.dart b/pkgs/dart_mcp/test/api/tools_test.dart
index 20f9a0d..d3ba813 100644
--- a/pkgs/dart_mcp/test/api/tools_test.dart
+++ b/pkgs/dart_mcp/test/api/tools_test.dart
@@ -17,7 +17,8 @@
// validate().
ValidationError onlyKeepError(ValidationError e) {
return ValidationError(
- e.error!, // The factory requires a non-nullable error.
+ e.error, // The factory requires a non-nullable error.
+ path: const [],
);
}
@@ -255,49 +256,49 @@
group('Type Mismatch', () {
test('object schema with non-map data', () {
expectFailuresMatch(Schema.object(), 'not a map', [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('list schema with non-list data', () {
expectFailuresMatch(Schema.list(), 'not a list', [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('string schema with non-string data', () {
expectFailuresMatch(Schema.string(), 123, [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('number schema with non-num data', () {
expectFailuresMatch(Schema.num(), 'not a number', [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('integer schema with non-int data', () {
expectFailuresMatch(Schema.int(), 'not an int', [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('integer schema with non-integer num data', () {
expectFailuresMatch(Schema.int(), 10.5, [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('boolean schema with non-bool data', () {
expectFailuresMatch(Schema.bool(), 'not a bool', [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('null schema with non-null data', () {
expectFailuresMatch(Schema.nil(), 'not null', [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('integer schema with integer-like num data (e.g. 10.0)', () {
// This test expects minimumNotMet because 10.0 is converted to int 10,
// which is less than the minimum of 11.
expectFailuresMatch(IntegerSchema(minimum: 11), 10.0, [
- ValidationError(ValidationErrorType.minimumNotMet),
+ ValidationError(ValidationErrorType.minimumNotMet, path: const []),
]);
});
});
@@ -310,8 +311,8 @@
// 'hi' fails minLength: 3.
// The allOf combinator fails, and the specific sub-failure is also reported.
expectFailuresMatch(schema, 'hi', [
- ValidationError(ValidationErrorType.allOfNotMet),
- ValidationError(ValidationErrorType.minLengthNotMet),
+ ValidationError(ValidationErrorType.allOfNotMet, path: const []),
+ ValidationError(ValidationErrorType.minLengthNotMet, path: const []),
]);
});
@@ -324,9 +325,9 @@
);
// 'Short123' fails minLength and pattern.
expectFailuresMatch(schema, 'Short123', [
- ValidationError(ValidationErrorType.allOfNotMet),
- ValidationError(ValidationErrorType.minLengthNotMet),
- ValidationError(ValidationErrorType.patternMismatch),
+ ValidationError(ValidationErrorType.allOfNotMet, path: const []),
+ ValidationError(ValidationErrorType.minLengthNotMet, path: const []),
+ ValidationError(ValidationErrorType.patternMismatch, path: const []),
]);
});
@@ -336,7 +337,7 @@
);
// `true` will cause typeMismatch for both StringSchema and NumberSchema.
expectFailuresMatch(schema, true, [
- ValidationError(ValidationErrorType.anyOfNotMet),
+ ValidationError(ValidationErrorType.anyOfNotMet, path: const []),
// The specific type mismatches from sub-schemas might also be reported
// depending on how _validateSchema handles anyOf error aggregation.
// Current tools.dart only adds anyOfNotMet for anyOf.
@@ -353,7 +354,7 @@
// "Hi1" fails minLength: 5 and pattern: '^[a-z]+$'.
// Since both sub-schemas fail, anyOfNotMet is reported.
expectFailuresMatch(schema, 'Hi1', [
- ValidationError(ValidationErrorType.anyOfNotMet),
+ ValidationError(ValidationErrorType.anyOfNotMet, path: const []),
]);
});
@@ -366,7 +367,7 @@
);
// `true` matches neither sub-schema.
expectFailuresMatch(s, true, [
- ValidationError(ValidationErrorType.oneOfNotMet),
+ ValidationError(ValidationErrorType.oneOfNotMet, path: const []),
]);
});
@@ -376,7 +377,7 @@
);
// 'test' matches both maxLength: 10 and pattern: 'test'.
expectFailuresMatch(schema, 'test', [
- ValidationError(ValidationErrorType.oneOfNotMet),
+ ValidationError(ValidationErrorType.oneOfNotMet, path: const []),
]);
});
@@ -386,7 +387,10 @@
);
// 'test' matches the second sub-schema in the "not" list.
expectFailuresMatch(schema, 'test', [
- ValidationError(ValidationErrorType.notConditionViolated),
+ ValidationError(
+ ValidationErrorType.notConditionViolated,
+ path: const [],
+ ),
]);
});
});
@@ -400,7 +404,8 @@
[
ValidationError(
ValidationErrorType.requiredPropertyMissing,
- details: 'Required property "name" is missing.',
+ path: const [],
+ details: 'Required property "name" is missing',
),
],
);
@@ -415,7 +420,12 @@
expectFailuresMatch(
schema,
{'name': 'test', 'age': 30},
- [ValidationError(ValidationErrorType.additionalPropertyNotAllowed)],
+ [
+ ValidationError(
+ ValidationErrorType.additionalPropertyNotAllowed,
+ path: const [],
+ ),
+ ],
);
});
@@ -429,9 +439,9 @@
schema,
{'name': 'test', 'extra': 'abc'},
[
- ValidationError(ValidationErrorType.additionalPropertyNotAllowed),
ValidationError(
ValidationErrorType.minLengthNotMet,
+ path: const [],
), // Sub-failure from additionalProperties schema
],
);
@@ -442,7 +452,12 @@
expectFailuresMatch(
schema,
{'a': 1},
- [ValidationError(ValidationErrorType.minPropertiesNotMet)],
+ [
+ ValidationError(
+ ValidationErrorType.minPropertiesNotMet,
+ path: const [],
+ ),
+ ],
);
});
@@ -451,7 +466,12 @@
expectFailuresMatch(
schema,
{'a': 1, 'b': 2},
- [ValidationError(ValidationErrorType.maxPropertiesExceeded)],
+ [
+ ValidationError(
+ ValidationErrorType.maxPropertiesExceeded,
+ path: const [],
+ ),
+ ],
);
});
@@ -462,9 +482,9 @@
schema,
{'ab': 1, 'abc': 2},
[
- ValidationError(ValidationErrorType.propertyNamesInvalid),
ValidationError(
ValidationErrorType.minLengthNotMet,
+ path: const [],
), // Sub-failure from propertyNames schema
],
);
@@ -479,10 +499,7 @@
expectFailuresMatch(
schema,
{'age': 10},
- [
- ValidationError(ValidationErrorType.propertyValueInvalid),
- ValidationError(ValidationErrorType.minimumNotMet),
- ],
+ [ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
});
@@ -494,10 +511,7 @@
expectFailuresMatch(
schema,
{'x-custom': 5},
- [
- ValidationError(ValidationErrorType.patternPropertyValueInvalid),
- ValidationError(ValidationErrorType.minimumNotMet),
- ],
+ [ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
});
@@ -510,7 +524,12 @@
expectFailuresMatch(
schema,
{'name': 'test', 'age': 30},
- [ValidationError(ValidationErrorType.unevaluatedPropertyNotAllowed)],
+ [
+ ValidationError(
+ ValidationErrorType.unevaluatedPropertyNotAllowed,
+ path: const [],
+ ),
+ ],
);
});
});
@@ -521,7 +540,7 @@
expectFailuresMatch(
schema,
[1],
- [ValidationError(ValidationErrorType.minItemsNotMet)],
+ [ValidationError(ValidationErrorType.minItemsNotMet, path: const [])],
);
});
@@ -530,7 +549,12 @@
expectFailuresMatch(
schema,
[1, 2],
- [ValidationError(ValidationErrorType.maxItemsExceeded)],
+ [
+ ValidationError(
+ ValidationErrorType.maxItemsExceeded,
+ path: const [],
+ ),
+ ],
);
});
@@ -539,7 +563,12 @@
expectFailuresMatch(
schema,
[1, 2, 1],
- [ValidationError(ValidationErrorType.uniqueItemsViolated)],
+ [
+ ValidationError(
+ ValidationErrorType.uniqueItemsViolated,
+ path: const [],
+ ),
+ ],
);
});
@@ -549,10 +578,7 @@
expectFailuresMatch(
schema,
[10, 5, 12],
- [
- ValidationError(ValidationErrorType.itemInvalid),
- ValidationError(ValidationErrorType.minimumNotMet),
- ],
+ [ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
});
@@ -564,18 +590,17 @@
expectFailuresMatch(
schema,
[5], // Not enough items for all prefixItems, but first one is checked
- [
- ValidationError(ValidationErrorType.prefixItemInvalid),
- ValidationError(ValidationErrorType.minimumNotMet),
- ],
+ [ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
// Second prefix item 'hi' fails StringSchema(minLength: 3).
expectFailuresMatch(
schema,
[10, 'hi'],
[
- ValidationError(ValidationErrorType.prefixItemInvalid),
- ValidationError(ValidationErrorType.minLengthNotMet),
+ ValidationError(
+ ValidationErrorType.minLengthNotMet,
+ path: const [],
+ ),
],
);
});
@@ -591,7 +616,12 @@
expectFailuresMatch(
schema,
[10, 'extra'],
- [ValidationError(ValidationErrorType.unevaluatedItemNotAllowed)],
+ [
+ ValidationError(
+ ValidationErrorType.unevaluatedItemNotAllowed,
+ path: const [],
+ ),
+ ],
);
},
);
@@ -601,7 +631,7 @@
test('minLengthNotMet', () {
final schema = StringSchema(minLength: 3);
expectFailuresMatch(schema, 'hi', [
- ValidationError(ValidationErrorType.minLengthNotMet),
+ ValidationError(ValidationErrorType.minLengthNotMet, path: const []),
]);
});
// ... other string specific tests using expectFailuresMatch
@@ -611,7 +641,7 @@
test('minimumNotMet', () {
final schema = NumberSchema(minimum: 10);
expectFailuresMatch(schema, 5, [
- ValidationError(ValidationErrorType.minimumNotMet),
+ ValidationError(ValidationErrorType.minimumNotMet, path: const []),
]);
});
// ... other number specific tests using expectFailuresMatch
@@ -621,7 +651,7 @@
test('minimumNotMet', () {
final schema = IntegerSchema(minimum: 10);
expectFailuresMatch(schema, 5, [
- ValidationError(ValidationErrorType.minimumNotMet),
+ ValidationError(ValidationErrorType.minimumNotMet, path: const []),
]);
});
// ... other integer specific tests using expectFailuresMatch
@@ -632,7 +662,11 @@
// Tests specifically for path validation will use expectFailuresExact
test('typeMismatch at root has empty path', () {
expectFailuresExact(Schema.string(), 123, [
- ValidationError(ValidationErrorType.typeMismatch, path: []),
+ ValidationError.typeMismatch(
+ path: [],
+ expectedType: String,
+ actualValue: 123,
+ ),
]);
});
@@ -646,7 +680,7 @@
ValidationErrorType.requiredPropertyMissing,
path:
[], // Missing property is checked at the current object level (root in this case)
- details: 'Required property "name" is missing.',
+ details: 'Required property "name" is missing',
),
],
);
@@ -661,13 +695,9 @@
{'age': 10},
[
ValidationError(
- ValidationErrorType.propertyValueInvalid,
- path: ['age'],
- ),
- ValidationError(
ValidationErrorType.minimumNotMet,
path: ['age'],
- details: 'Value 10 is less than the minimum of 18.',
+ details: 'Value 10 is less than the minimum of 18',
), // Sub-failure also has the path
],
);
@@ -694,23 +724,12 @@
}, // 'street' is missing
[
ValidationError(
- ValidationErrorType.propertyValueInvalid,
- path: [
- 'user',
- 'address',
- ], // Path to the object where 'street' is missing
- ),
- ValidationError(
- ValidationErrorType.propertyValueInvalid,
- path: ['user'], // Path to the object where 'street' is missing
- ),
- ValidationError(
ValidationErrorType.requiredPropertyMissing,
path: [
'user',
'address',
], // Path to the object where 'street' is missing
- details: 'Required property "street" is missing.',
+ details: 'Required property "street" is missing',
),
],
);
@@ -722,11 +741,10 @@
schema,
[101, 50, 200], // Item at index 1 (value 50) is invalid
[
- ValidationError(ValidationErrorType.itemInvalid, path: ['1']),
ValidationError(
ValidationErrorType.minimumNotMet,
path: ['1'],
- details: 'Value 50 is less than the minimum of 100.',
+ details: 'Value 50 is less than the minimum of 100',
),
],
);
@@ -740,17 +758,15 @@
schema,
['ok', 20], // Item at index 1 (value 20) fails prefixItem schema
[
- ValidationError(ValidationErrorType.prefixItemInvalid, path: ['0']),
ValidationError(
ValidationErrorType.minLengthNotMet,
path: ['0'],
- details: 'String "ok" is not at least 3 characters long.',
+ details: 'String "ok" is not at least 3 characters long',
),
- ValidationError(ValidationErrorType.prefixItemInvalid, path: ['1']),
ValidationError(
ValidationErrorType.maximumExceeded,
path: ['1'],
- details: 'Value 20 is more than the maximum of 10.',
+ details: 'Value 20 is more than the maximum of 10',
),
],
);
@@ -766,12 +782,12 @@
ValidationError(
ValidationErrorType.minLengthNotMet,
path: [],
- details: 'String "hi" is not at least 3 characters long.',
+ details: 'String "hi" is not at least 3 characters long',
), // from first sub-schema
ValidationError(
ValidationErrorType.maxLengthExceeded,
path: [],
- details: 'String "hi" is more than 1 characters long.',
+ details: 'String "hi" is more than 1 characters long',
), // from second sub-schema
]);
});
@@ -786,13 +802,9 @@
{'name': 'test', 'extra': 'abc'},
[
ValidationError(
- ValidationErrorType.additionalPropertyNotAllowed,
- path: ['extra'],
- ),
- ValidationError(
ValidationErrorType.minLengthNotMet,
path: ['extra'],
- details: 'String "abc" is not at least 5 characters long.',
+ details: 'String "abc" is not at least 5 characters long',
),
],
);
@@ -803,47 +815,47 @@
group('Type Mismatch', () {
test('object schema with non-map data', () {
expectFailuresMatch(Schema.object(), 'not a map', [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('list schema with non-list data', () {
expectFailuresMatch(Schema.list(), 'not a list', [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('string schema with non-string data', () {
expectFailuresMatch(Schema.string(), 123, [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('number schema with non-num data', () {
expectFailuresMatch(Schema.num(), 'not a number', [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('integer schema with non-int data', () {
expectFailuresMatch(Schema.int(), 'not an int', [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('integer schema with non-integer num data', () {
expectFailuresMatch(Schema.int(), 10.5, [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('boolean schema with non-bool data', () {
expectFailuresMatch(Schema.bool(), 'not a bool', [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('null schema with non-null data', () {
expectFailuresMatch(Schema.nil(), 'not null', [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
test('integer schema with integer-like num data (e.g. 10.0)', () {
expectFailuresMatch(IntegerSchema(minimum: 11), 10.0, [
- ValidationError(ValidationErrorType.minimumNotMet),
+ ValidationError(ValidationErrorType.minimumNotMet, path: const []),
]);
});
});
@@ -852,7 +864,10 @@
test('enumValueNotAllowed', () {
final schema = EnumSchema(values: {'a', 'b'});
expectFailuresMatch(schema, 'c', [
- ValidationError(ValidationErrorType.enumValueNotAllowed),
+ ValidationError(
+ ValidationErrorType.enumValueNotAllowed,
+ path: const [],
+ ),
]);
});
@@ -864,7 +879,7 @@
test('enum with non-string data', () {
final schema = EnumSchema(values: {'a', 'b'});
expectFailuresMatch(schema, 1, [
- ValidationError(ValidationErrorType.typeMismatch),
+ ValidationError(ValidationErrorType.typeMismatch, path: const []),
]);
});
});
@@ -875,8 +890,8 @@
allOf: [StringSchema(minLength: 3), StringSchema(maxLength: 5)],
);
expectFailuresMatch(schema, 'hi', [
- ValidationError(ValidationErrorType.allOfNotMet),
- ValidationError(ValidationErrorType.minLengthNotMet),
+ ValidationError(ValidationErrorType.allOfNotMet, path: const []),
+ ValidationError(ValidationErrorType.minLengthNotMet, path: const []),
]);
});
@@ -888,9 +903,9 @@
],
);
expectFailuresMatch(schema, 'Short123', [
- ValidationError(ValidationErrorType.allOfNotMet),
- ValidationError(ValidationErrorType.minLengthNotMet),
- ValidationError(ValidationErrorType.patternMismatch),
+ ValidationError(ValidationErrorType.allOfNotMet, path: const []),
+ ValidationError(ValidationErrorType.minLengthNotMet, path: const []),
+ ValidationError(ValidationErrorType.patternMismatch, path: const []),
]);
});
@@ -904,7 +919,7 @@
// NumberSchema(minimum: 100).validate(true) -> [typeMismatch]
// So anyOf fails.
expectFailuresMatch(schema, true, [
- ValidationError(ValidationErrorType.anyOfNotMet),
+ ValidationError(ValidationErrorType.anyOfNotMet, path: const []),
]);
});
@@ -920,7 +935,7 @@
// StringSchema(pattern: '^[a-z]+$').validate("Hi1") -> [patternMismatch]
// Since both fail, anyOf fails.
expectFailuresMatch(schema, 'Hi1', [
- ValidationError(ValidationErrorType.anyOfNotMet),
+ ValidationError(ValidationErrorType.anyOfNotMet, path: const []),
]);
});
@@ -937,7 +952,7 @@
],
);
expectFailuresMatch(s, true, [
- ValidationError(ValidationErrorType.oneOfNotMet),
+ ValidationError(ValidationErrorType.oneOfNotMet, path: const []),
]);
});
@@ -946,7 +961,7 @@
oneOf: [StringSchema(maxLength: 10), StringSchema(pattern: 'test')],
);
expectFailuresMatch(schema, 'test', [
- ValidationError(ValidationErrorType.oneOfNotMet),
+ ValidationError(ValidationErrorType.oneOfNotMet, path: const []),
]);
});
@@ -955,7 +970,10 @@
not: [StringSchema(maxLength: 2), StringSchema(pattern: 'test')],
);
expectFailuresMatch(schema, 'test', [
- ValidationError(ValidationErrorType.notConditionViolated),
+ ValidationError(
+ ValidationErrorType.notConditionViolated,
+ path: const [],
+ ),
]);
});
@@ -964,7 +982,14 @@
not: [StringSchema(maxLength: 10), StringSchema(pattern: 'test')],
);
expectFailuresMatch(schema, 'test', [
- ValidationError(ValidationErrorType.notConditionViolated),
+ ValidationError(
+ ValidationErrorType.notConditionViolated,
+ path: const [],
+ ),
+ ValidationError(
+ ValidationErrorType.notConditionViolated,
+ path: const [],
+ ),
]);
});
});
@@ -975,7 +1000,12 @@
expectFailuresMatch(
schema,
{'foo': 1},
- [ValidationError(ValidationErrorType.requiredPropertyMissing)],
+ [
+ ValidationError(
+ ValidationErrorType.requiredPropertyMissing,
+ path: const [],
+ ),
+ ],
);
});
@@ -987,7 +1017,12 @@
expectFailuresMatch(
schema,
{'name': 'test', 'age': 30},
- [ValidationError(ValidationErrorType.additionalPropertyNotAllowed)],
+ [
+ ValidationError(
+ ValidationErrorType.additionalPropertyNotAllowed,
+ path: const [],
+ ),
+ ],
);
});
@@ -1000,8 +1035,10 @@
schema,
{'name': 'test', 'extra': 'abc'},
[
- ValidationError(ValidationErrorType.minLengthNotMet),
- ValidationError(ValidationErrorType.additionalPropertyNotAllowed),
+ ValidationError(
+ ValidationErrorType.minLengthNotMet,
+ path: const [],
+ ),
],
);
});
@@ -1011,7 +1048,12 @@
expectFailuresMatch(
schema,
{'a': 1},
- [ValidationError(ValidationErrorType.minPropertiesNotMet)],
+ [
+ ValidationError(
+ ValidationErrorType.minPropertiesNotMet,
+ path: const [],
+ ),
+ ],
);
});
@@ -1020,7 +1062,12 @@
expectFailuresMatch(
schema,
{'a': 1, 'b': 2},
- [ValidationError(ValidationErrorType.maxPropertiesExceeded)],
+ [
+ ValidationError(
+ ValidationErrorType.maxPropertiesExceeded,
+ path: const [],
+ ),
+ ],
);
});
@@ -1030,8 +1077,10 @@
schema,
{'ab': 1, 'abc': 2},
[
- ValidationError(ValidationErrorType.minLengthNotMet),
- ValidationError(ValidationErrorType.propertyNamesInvalid),
+ ValidationError(
+ ValidationErrorType.minLengthNotMet,
+ path: const [],
+ ),
],
);
});
@@ -1043,10 +1092,7 @@
expectFailuresMatch(
schema,
{'age': 10},
- [
- ValidationError(ValidationErrorType.propertyValueInvalid),
- ValidationError(ValidationErrorType.minimumNotMet),
- ],
+ [ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
});
@@ -1057,10 +1103,7 @@
expectFailuresMatch(
schema,
{'x-custom': 5},
- [
- ValidationError(ValidationErrorType.patternPropertyValueInvalid),
- ValidationError(ValidationErrorType.minimumNotMet),
- ],
+ [ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
});
@@ -1073,7 +1116,12 @@
expectFailuresMatch(
schema,
{'name': 'test', 'age': 30},
- [ValidationError(ValidationErrorType.unevaluatedPropertyNotAllowed)],
+ [
+ ValidationError(
+ ValidationErrorType.unevaluatedPropertyNotAllowed,
+ path: const [],
+ ),
+ ],
);
});
@@ -1107,7 +1155,7 @@
expectFailuresMatch(
schema,
[1],
- [ValidationError(ValidationErrorType.minItemsNotMet)],
+ [ValidationError(ValidationErrorType.minItemsNotMet, path: const [])],
);
});
@@ -1116,7 +1164,12 @@
expectFailuresMatch(
schema,
[1, 2],
- [ValidationError(ValidationErrorType.maxItemsExceeded)],
+ [
+ ValidationError(
+ ValidationErrorType.maxItemsExceeded,
+ path: const [],
+ ),
+ ],
);
});
@@ -1125,7 +1178,12 @@
expectFailuresMatch(
schema,
[1, 2, 1],
- [ValidationError(ValidationErrorType.uniqueItemsViolated)],
+ [
+ ValidationError(
+ ValidationErrorType.uniqueItemsViolated,
+ path: const [],
+ ),
+ ],
);
});
@@ -1136,10 +1194,7 @@
expectFailuresMatch(
schema,
[10, 5, 12],
- [
- ValidationError(ValidationErrorType.itemInvalid),
- ValidationError(ValidationErrorType.minimumNotMet),
- ],
+ [ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
});
@@ -1150,17 +1205,16 @@
expectFailuresMatch(
schema,
[5],
- [
- ValidationError(ValidationErrorType.prefixItemInvalid),
- ValidationError(ValidationErrorType.minimumNotMet),
- ],
+ [ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
expectFailuresMatch(
schema,
[10, 'hi'],
[
- ValidationError(ValidationErrorType.prefixItemInvalid),
- ValidationError(ValidationErrorType.minLengthNotMet),
+ ValidationError(
+ ValidationErrorType.minLengthNotMet,
+ path: const [],
+ ),
],
);
});
@@ -1175,7 +1229,12 @@
expectFailuresMatch(
schema,
[10, 'extra'],
- [ValidationError(ValidationErrorType.unevaluatedItemNotAllowed)],
+ [
+ ValidationError(
+ ValidationErrorType.unevaluatedItemNotAllowed,
+ path: const [],
+ ),
+ ],
);
},
);
@@ -1185,7 +1244,12 @@
expectFailuresMatch(
schema,
['extra'],
- [ValidationError(ValidationErrorType.unevaluatedItemNotAllowed)],
+ [
+ ValidationError(
+ ValidationErrorType.unevaluatedItemNotAllowed,
+ path: const [],
+ ),
+ ],
);
});
@@ -1208,8 +1272,10 @@
schemaWithItems,
[10, 'a'],
[
- ValidationError(ValidationErrorType.itemInvalid),
- ValidationError(ValidationErrorType.minLengthNotMet),
+ ValidationError(
+ ValidationErrorType.minLengthNotMet,
+ path: const [],
+ ),
],
);
@@ -1222,7 +1288,12 @@
expectFailuresMatch(
schemaNoItems,
[10, 'extra string'],
- [ValidationError(ValidationErrorType.unevaluatedItemNotAllowed)],
+ [
+ ValidationError(
+ ValidationErrorType.unevaluatedItemNotAllowed,
+ path: const [],
+ ),
+ ],
);
});
@@ -1236,10 +1307,7 @@
expectFailuresMatch(
schema,
[10, 'hello', true], // `true` is unevaluated but allowed
- [
- ValidationError(ValidationErrorType.itemInvalid),
- ValidationError(ValidationErrorType.typeMismatch),
- ],
+ [ValidationError(ValidationErrorType.typeMismatch, path: const [])],
reason:
'Item `true` at index 2 is evaluated by `items: StringSchema()` '
'and fails. `unevaluatedItems` (defaulting to true) does not apply '
@@ -1252,21 +1320,24 @@
test('minLengthNotMet', () {
final schema = StringSchema(minLength: 3);
expectFailuresMatch(schema, 'hi', [
- ValidationError(ValidationErrorType.minLengthNotMet),
+ ValidationError(ValidationErrorType.minLengthNotMet, path: const []),
]);
});
test('maxLengthExceeded', () {
final schema = StringSchema(maxLength: 3);
expectFailuresMatch(schema, 'hello', [
- ValidationError(ValidationErrorType.maxLengthExceeded),
+ ValidationError(
+ ValidationErrorType.maxLengthExceeded,
+ path: const [],
+ ),
]);
});
test('patternMismatch', () {
final schema = StringSchema(pattern: r'^\d+$');
expectFailuresMatch(schema, 'abc', [
- ValidationError(ValidationErrorType.patternMismatch),
+ ValidationError(ValidationErrorType.patternMismatch, path: const []),
]);
});
});
@@ -1275,53 +1346,71 @@
test('minimumNotMet', () {
final schema = NumberSchema(minimum: 10);
expectFailuresMatch(schema, 5, [
- ValidationError(ValidationErrorType.minimumNotMet),
+ ValidationError(ValidationErrorType.minimumNotMet, path: const []),
]);
});
test('maximumExceeded', () {
final schema = NumberSchema(maximum: 10);
expectFailuresMatch(schema, 15, [
- ValidationError(ValidationErrorType.maximumExceeded),
+ ValidationError(ValidationErrorType.maximumExceeded, path: const []),
]);
});
test('exclusiveMinimumNotMet - equal value', () {
final schema = NumberSchema(exclusiveMinimum: 10);
expectFailuresMatch(schema, 10, [
- ValidationError(ValidationErrorType.exclusiveMinimumNotMet),
+ ValidationError(
+ ValidationErrorType.exclusiveMinimumNotMet,
+ path: const [],
+ ),
]);
});
test('exclusiveMinimumNotMet - smaller value', () {
final schema = NumberSchema(exclusiveMinimum: 10);
expectFailuresMatch(schema, 9, [
- ValidationError(ValidationErrorType.exclusiveMinimumNotMet),
+ ValidationError(
+ ValidationErrorType.exclusiveMinimumNotMet,
+ path: const [],
+ ),
]);
});
test('exclusiveMaximumExceeded - equal value', () {
final schema = NumberSchema(exclusiveMaximum: 10);
expectFailuresMatch(schema, 10, [
- ValidationError(ValidationErrorType.exclusiveMaximumExceeded),
+ ValidationError(
+ ValidationErrorType.exclusiveMaximumExceeded,
+ path: const [],
+ ),
]);
});
test('exclusiveMaximumExceeded - larger value', () {
final schema = NumberSchema(exclusiveMaximum: 10);
expectFailuresMatch(schema, 11, [
- ValidationError(ValidationErrorType.exclusiveMaximumExceeded),
+ ValidationError(
+ ValidationErrorType.exclusiveMaximumExceeded,
+ path: const [],
+ ),
]);
});
test('multipleOfInvalid', () {
final schema = NumberSchema(multipleOf: 3);
expectFailuresMatch(schema, 10, [
- ValidationError(ValidationErrorType.multipleOfInvalid),
+ ValidationError(
+ ValidationErrorType.multipleOfInvalid,
+ path: const [],
+ ),
]);
});
test('multipleOfInvalid - floating point', () {
final schema = NumberSchema(multipleOf: 0.1);
expectFailuresMatch(schema, 0.25, [
- ValidationError(ValidationErrorType.multipleOfInvalid),
+ ValidationError(
+ ValidationErrorType.multipleOfInvalid,
+ path: const [],
+ ),
]);
});
test('multipleOfInvalid - valid floating point', () {
@@ -1347,47 +1436,62 @@
test('minimumNotMet', () {
final schema = IntegerSchema(minimum: 10);
expectFailuresMatch(schema, 5, [
- ValidationError(ValidationErrorType.minimumNotMet),
+ ValidationError(ValidationErrorType.minimumNotMet, path: const []),
]);
});
test('maximumExceeded', () {
final schema = IntegerSchema(maximum: 10);
expectFailuresMatch(schema, 15, [
- ValidationError(ValidationErrorType.maximumExceeded),
+ ValidationError(ValidationErrorType.maximumExceeded, path: const []),
]);
});
test('exclusiveMinimumNotMet - equal value', () {
final schema = IntegerSchema(exclusiveMinimum: 10);
expectFailuresMatch(schema, 10, [
- ValidationError(ValidationErrorType.exclusiveMinimumNotMet),
+ ValidationError(
+ ValidationErrorType.exclusiveMinimumNotMet,
+ path: const [],
+ ),
]);
});
test('exclusiveMinimumNotMet - smaller value', () {
final schema = IntegerSchema(exclusiveMinimum: 10);
expectFailuresMatch(schema, 9, [
- ValidationError(ValidationErrorType.exclusiveMinimumNotMet),
+ ValidationError(
+ ValidationErrorType.exclusiveMinimumNotMet,
+ path: const [],
+ ),
]);
});
test('exclusiveMaximumExceeded - equal value', () {
final schema = IntegerSchema(exclusiveMaximum: 10);
expectFailuresMatch(schema, 10, [
- ValidationError(ValidationErrorType.exclusiveMaximumExceeded),
+ ValidationError(
+ ValidationErrorType.exclusiveMaximumExceeded,
+ path: const [],
+ ),
]);
});
test('exclusiveMaximumExceeded - larger value', () {
final schema = IntegerSchema(exclusiveMaximum: 10);
expectFailuresMatch(schema, 11, [
- ValidationError(ValidationErrorType.exclusiveMaximumExceeded),
+ ValidationError(
+ ValidationErrorType.exclusiveMaximumExceeded,
+ path: const [],
+ ),
]);
});
test('multipleOfInvalid', () {
final schema = IntegerSchema(multipleOf: 3);
expectFailuresMatch(schema, 10, [
- ValidationError(ValidationErrorType.multipleOfInvalid),
+ ValidationError(
+ ValidationErrorType.multipleOfInvalid,
+ path: const [],
+ ),
]);
});
@@ -1421,12 +1525,12 @@
final schemaAnyOfEmpty = Schema.combined(anyOf: []);
expectFailuresMatch(schemaAnyOfEmpty, 'any data', [
- ValidationError(ValidationErrorType.anyOfNotMet),
+ ValidationError(ValidationErrorType.anyOfNotMet, path: const []),
]);
final schemaOneOfEmpty = Schema.combined(oneOf: []);
expectFailuresMatch(schemaOneOfEmpty, 'any data', [
- ValidationError(ValidationErrorType.oneOfNotMet),
+ ValidationError(ValidationErrorType.oneOfNotMet, path: const []),
]);
// If 'not' is a list of schemas, and the list is empty,
@@ -1458,16 +1562,16 @@
// Fails minLength (from parent StringSchema) and pattern (from allOf)
expectFailuresExact(schema, 'A', [
+ ValidationError(ValidationErrorType.allOfNotMet, path: []),
ValidationError(
ValidationErrorType.minLengthNotMet,
path: [],
- details: 'String "A" is not at least 2 characters long.',
+ details: 'String "A" is not at least 2 characters long',
),
- ValidationError(ValidationErrorType.allOfNotMet, path: []),
ValidationError(
ValidationErrorType.patternMismatch,
path: [],
- details: 'String "A" doesn\'t match the pattern "^[a-z]+\$".',
+ details: 'String "A" doesn\'t match the pattern "^[a-z]+\$"',
),
]);
@@ -1477,7 +1581,7 @@
ValidationError(
ValidationErrorType.maxLengthExceeded,
path: [],
- details: 'String "abcdef" is more than 5 characters long.',
+ details: 'String "abcdef" is more than 5 characters long',
),
]);
});
@@ -1501,17 +1605,9 @@
},
[
ValidationError(
- ValidationErrorType.propertyValueInvalid,
- path: ['user'],
- ),
- ValidationError(
- ValidationErrorType.propertyValueInvalid,
- path: ['user', 'name'],
- ),
- ValidationError(
ValidationErrorType.minLengthNotMet,
path: ['user', 'name'],
- details: 'String "hi" is not at least 5 characters long.',
+ details: 'String "hi" is not at least 5 characters long',
),
],
);
@@ -1531,15 +1627,10 @@
{'id': 10}, // This item is invalid
],
[
- ValidationError(ValidationErrorType.itemInvalid, path: ['1']),
- ValidationError(
- ValidationErrorType.propertyValueInvalid,
- path: ['1', 'id'],
- ),
ValidationError(
ValidationErrorType.minimumNotMet,
path: ['1', 'id'],
- details: 'Value 10 is less than the minimum of 100.',
+ details: 'Value 10 is less than the minimum of 100',
),
],
);
@@ -1556,19 +1647,13 @@
expectFailuresMatch(
schema,
{'known': 'yes', 'extraNum': -5},
- [
- ValidationError(ValidationErrorType.minimumNotMet),
- ValidationError(ValidationErrorType.additionalPropertyNotAllowed),
- ],
+ [ValidationError(ValidationErrorType.minimumNotMet, path: const [])],
);
// Invalid: additional property is wrong type for its schema
expectFailuresMatch(
schema,
{'known': 'yes', 'extraStr': 'text'},
- [
- ValidationError(ValidationErrorType.typeMismatch),
- ValidationError(ValidationErrorType.additionalPropertyNotAllowed),
- ],
+ [ValidationError(ValidationErrorType.typeMismatch, path: const [])],
);
});
@@ -1583,7 +1668,12 @@
expectFailuresMatch(
schema,
{'y-foo': 'bar'},
- [ValidationError(ValidationErrorType.unevaluatedPropertyNotAllowed)],
+ [
+ ValidationError(
+ ValidationErrorType.unevaluatedPropertyNotAllowed,
+ path: const [],
+ ),
+ ],
);
});
@@ -1631,8 +1721,10 @@
schema,
{'name': 'test', 'age': 30},
[
- ValidationError(ValidationErrorType.additionalPropertyNotAllowed),
- ValidationError(ValidationErrorType.minimumNotMet),
+ ValidationError(
+ ValidationErrorType.minimumNotMet,
+ path: const [],
+ ),
],
);
},
@@ -1650,10 +1742,7 @@
expectFailuresMatch(
schema,
[1, 'b', 3],
- [
- ValidationError(ValidationErrorType.itemInvalid),
- ValidationError(ValidationErrorType.typeMismatch),
- ],
+ [ValidationError(ValidationErrorType.typeMismatch, path: const [])],
);
});
@@ -1671,19 +1760,13 @@
expectFailuresMatch(
schema,
['a', 1, 'c'],
- [
- ValidationError(ValidationErrorType.itemInvalid),
- ValidationError(ValidationErrorType.typeMismatch),
- ],
+ [ValidationError(ValidationErrorType.typeMismatch, path: const [])],
);
// Invalid: prefixItem fails StringSchema
expectFailuresMatch(
schema,
[10, 1, 2],
- [
- ValidationError(ValidationErrorType.prefixItemInvalid),
- ValidationError(ValidationErrorType.typeMismatch),
- ],
+ [ValidationError(ValidationErrorType.typeMismatch, path: const [])],
);
},
);
@@ -1720,13 +1803,13 @@
ValidationError(
ValidationErrorType.minLengthNotMet,
path: [],
- details: 'String "Hi" is not at least 5 characters long.',
+ details: 'String "Hi" is not at least 5 characters long',
),
ValidationError(ValidationErrorType.allOfNotMet, path: []),
ValidationError(
ValidationErrorType.patternMismatch,
path: [],
- details: 'String "Hi" doesn\'t match the pattern "^[a-z]+\$".',
+ details: 'String "Hi" doesn\'t match the pattern "^[a-z]+\$"',
),
]);
@@ -1738,7 +1821,7 @@
ValidationError(
ValidationErrorType.patternMismatch,
path: [],
- details: 'String "Hiall" doesn\'t match the pattern "^[a-z]+\$".',
+ details: 'String "Hiall" doesn\'t match the pattern "^[a-z]+\$"',
),
]);
@@ -1760,7 +1843,7 @@
ValidationErrorType.patternMismatch,
path: [],
details:
- 'String "LongEnoughButCAPS" doesn\'t match the pattern "^[a-z]+\$".',
+ 'String "LongEnoughButCAPS" doesn\'t match the pattern "^[a-z]+\$"',
),
]);
},
diff --git a/pkgs/dart_mcp/test/server/tools_support_test.dart b/pkgs/dart_mcp/test/server/tools_support_test.dart
index 75721eb..337082a 100644
--- a/pkgs/dart_mcp/test/server/tools_support_test.dart
+++ b/pkgs/dart_mcp/test/server/tools_support_test.dart
@@ -24,9 +24,11 @@
final serverConnection = environment.serverConnection;
final toolsResult = await serverConnection.listTools();
- expect(toolsResult.tools.length, 1);
+ expect(toolsResult.tools.length, 2);
- final tool = toolsResult.tools.single;
+ final tool = toolsResult.tools.firstWhere(
+ (tool) => tool.name == TestMCPServerWithTools.helloWorld.name,
+ );
final result = await serverConnection.callTool(
CallToolRequest(name: tool.name),
@@ -72,6 +74,46 @@
// Need to manually close so the stream matchers can complete.
await environment.shutdown();
});
+
+ test('schema validation failure returns an error', () async {
+ final environment = TestEnvironment(
+ TestMCPClient(),
+ TestMCPServerWithTools.new,
+ );
+ await environment.initializeServer();
+
+ final serverConnection = environment.serverConnection;
+
+ // Call with no arguments, should fail because 'message' is required.
+ var result = await serverConnection.callTool(
+ CallToolRequest(
+ name: TestMCPServerWithTools.echo.name,
+ arguments: const {},
+ ),
+ );
+ expect(result.isError, isTrue);
+ expect(result.content.single, isA<TextContent>());
+ final textContent = result.content.single as TextContent;
+ expect(
+ textContent.text,
+ contains('Required property "message" is missing at path #root'),
+ );
+
+ // Call with wrong type for 'message'.
+ result = await serverConnection.callTool(
+ CallToolRequest(
+ name: TestMCPServerWithTools.echo.name,
+ arguments: {'message': 123},
+ ),
+ );
+ expect(result.isError, isTrue);
+ expect(result.content.single, isA<TextContent>());
+ final textContent2 = result.content.single as TextContent;
+ expect(
+ textContent2.text,
+ contains('Value `123` is not of type `String` at path #root["message"]'),
+ );
+ });
}
final class TestMCPServerWithTools extends TestMCPServer with ToolsSupport {
@@ -83,9 +125,24 @@
helloWorld,
(_) => CallToolResult(content: [helloWorldContent]),
);
+ registerTool(TestMCPServerWithTools.echo, TestMCPServerWithTools.echoImpl);
return super.initialize(request);
}
+ static final echo = Tool(
+ name: 'echo',
+ description: 'Echoes the input',
+ inputSchema: ObjectSchema(
+ properties: {'message': StringSchema(description: 'The message to echo')},
+ required: ['message'],
+ ),
+ );
+
+ static CallToolResult echoImpl(CallToolRequest request) {
+ final message = request.arguments!['message'] as String;
+ return CallToolResult(content: [TextContent(text: message)]);
+ }
+
static final helloWorld = Tool(
name: 'hello_world',
description: 'Says hello world!',
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart b/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart
index 70fc5a6..ef4bb55 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/dash_cli.dart
@@ -96,7 +96,7 @@
if (projectType != 'dart' && projectType != 'flutter') {
errors.add(
ValidationError(
- ValidationErrorType.itemInvalid,
+ ValidationErrorType.custom,
path: [ParameterNames.projectType],
details: 'Only `dart` and `flutter` are allowed values.',
),
@@ -106,7 +106,7 @@
if (p.isAbsolute(directory)) {
errors.add(
ValidationError(
- ValidationErrorType.itemInvalid,
+ ValidationErrorType.custom,
path: [ParameterNames.directory],
details: 'Directory must be a relative path.',
),
@@ -125,7 +125,7 @@
: 'is not a valid platform';
errors.add(
ValidationError(
- ValidationErrorType.itemInvalid,
+ ValidationErrorType.custom,
path: [ParameterNames.platform],
details:
'${invalidPlatforms.join(',')} $plural. Platforms '
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart
index c390004..8fe2f8e 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/dtd.dart
@@ -187,15 +187,6 @@
return _dtdAlreadyConnected;
}
- if (request.arguments?[ParameterNames.uri] == null) {
- return CallToolResult(
- isError: true,
- content: [
- TextContent(text: 'Required parameter "uri" was not provided.'),
- ],
- );
- }
-
try {
_dtd = await DartToolingDaemon.connect(
Uri.parse(request.arguments![ParameterNames.uri] as String),
diff --git a/pkgs/dart_mcp_server/lib/src/mixins/pub.dart b/pkgs/dart_mcp_server/lib/src/mixins/pub.dart
index ab48500..9059a10 100644
--- a/pkgs/dart_mcp_server/lib/src/mixins/pub.dart
+++ b/pkgs/dart_mcp_server/lib/src/mixins/pub.dart
@@ -34,13 +34,7 @@
/// Implementation of the [pubTool].
Future<CallToolResult> _runDartPubTool(CallToolRequest request) async {
- final command = request.arguments?[ParameterNames.command] as String?;
- if (command == null) {
- return CallToolResult(
- content: [TextContent(text: 'Missing required argument `command`.')],
- isError: true,
- );
- }
+ final command = request.arguments![ParameterNames.command] as String;
final matchingCommand = SupportedPubCommand.fromName(command);
if (matchingCommand == null) {
return CallToolResult(
diff --git a/pkgs/dart_mcp_server/lib/src/server.dart b/pkgs/dart_mcp_server/lib/src/server.dart
index f90a80f..04af79d 100644
--- a/pkgs/dart_mcp_server/lib/src/server.dart
+++ b/pkgs/dart_mcp_server/lib/src/server.dart
@@ -161,8 +161,9 @@
/// if [analytics] is not `null`.
void registerTool(
Tool tool,
- FutureOr<CallToolResult> Function(CallToolRequest) impl,
- ) {
+ FutureOr<CallToolResult> Function(CallToolRequest) impl, {
+ bool validateArguments = true,
+ }) {
// For type promotion.
final analytics = this.analytics;
@@ -196,6 +197,7 @@
}
}
},
+ validateArguments: validateArguments,
);
}
diff --git a/pkgs/dart_mcp_server/pubspec.yaml b/pkgs/dart_mcp_server/pubspec.yaml
index 4df5932..0f37a2b 100644
--- a/pkgs/dart_mcp_server/pubspec.yaml
+++ b/pkgs/dart_mcp_server/pubspec.yaml
@@ -13,7 +13,7 @@
args: ^2.7.0
async: ^2.13.0
collection: ^1.19.1
- dart_mcp: ^0.2.2
+ dart_mcp: ^0.3.0
dds_service_extensions: ^2.0.1
devtools_shared: ^11.2.0
dtd: ^2.4.0
diff --git a/pkgs/dart_mcp_server/test/tools/dtd_test.dart b/pkgs/dart_mcp_server/test/tools/dtd_test.dart
index 713fd1a..bf92e23 100644
--- a/pkgs/dart_mcp_server/test/tools/dtd_test.dart
+++ b/pkgs/dart_mcp_server/test/tools/dtd_test.dart
@@ -607,7 +607,7 @@
expect(missingArgResult.isError, isTrue);
expect(
(missingArgResult.content.first as TextContent).text,
- 'Required parameter "enabled" was not provided or is not a boolean.',
+ 'Required property "enabled" is missing at path #root',
);
// Clean up
diff --git a/pkgs/dart_mcp_server/test/tools/pub_test.dart b/pkgs/dart_mcp_server/test/tools/pub_test.dart
index 841a60d..e349db1 100644
--- a/pkgs/dart_mcp_server/test/tools/pub_test.dart
+++ b/pkgs/dart_mcp_server/test/tools/pub_test.dart
@@ -192,7 +192,7 @@
expect(
(result.content.single as TextContent).text,
- 'Missing required argument `command`.',
+ 'Required property "command" is missing at path #root',
);
expect(testProcessManager.commandsRan, isEmpty);
});
diff --git a/pkgs/dart_mcp_server/test_fixtures/counter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/pkgs/dart_mcp_server/test_fixtures/counter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
new file mode 100644
index 0000000..539ab02
--- /dev/null
+++ b/pkgs/dart_mcp_server/test_fixtures/counter_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java
@@ -0,0 +1,19 @@
+package io.flutter.plugins;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import io.flutter.Log;
+
+import io.flutter.embedding.engine.FlutterEngine;
+
+/**
+ * Generated file. Do not edit.
+ * This file is generated by the Flutter tool based on the
+ * plugins that support the Android platform.
+ */
+@Keep
+public final class GeneratedPluginRegistrant {
+ private static final String TAG = "GeneratedPluginRegistrant";
+ public static void registerWith(@NonNull FlutterEngine flutterEngine) {
+ }
+}
diff --git a/pkgs/dart_mcp_server/test_fixtures/counter_app/android/local.properties b/pkgs/dart_mcp_server/test_fixtures/counter_app/android/local.properties
new file mode 100644
index 0000000..be68df0
--- /dev/null
+++ b/pkgs/dart_mcp_server/test_fixtures/counter_app/android/local.properties
@@ -0,0 +1 @@
+flutter.sdk=/usr/local/google/home/jakemac/flutter
\ No newline at end of file