Add support for emitting usage as a JSON schema
- Vendor a copy of a library defining a JSON schema API which can output
as a `Map<String, Object?>`.
- Add a `jsonSchema` getter on `ArgParser` which is similar to `usage`
but outputs a schema instead of help text.
Clients can manually support a `--json-help` or similar argument in the
same way they support `--help`.
diff --git a/pkgs/args/lib/src/allow_anything_parser.dart b/pkgs/args/lib/src/allow_anything_parser.dart
index 69472b3..fb62fe9 100644
--- a/pkgs/args/lib/src/allow_anything_parser.dart
+++ b/pkgs/args/lib/src/allow_anything_parser.dart
@@ -23,6 +23,10 @@
int? get usageLineLength => null;
@override
+ Map<String, Object?> get jsonSchema =>
+ const {'type': 'object', 'properties': {}};
+
+ @override
ArgParser addCommand(String name, [ArgParser? parser]) {
throw UnsupportedError(
"ArgParser.allowAnything().addCommands() isn't supported.");
diff --git a/pkgs/args/lib/src/arg_parser.dart b/pkgs/args/lib/src/arg_parser.dart
index 37041d7..4652fc9 100644
--- a/pkgs/args/lib/src/arg_parser.dart
+++ b/pkgs/args/lib/src/arg_parser.dart
@@ -6,6 +6,7 @@
import 'allow_anything_parser.dart';
import 'arg_results.dart';
+import 'json_schema.dart';
import 'option.dart';
import 'parser.dart';
import 'usage.dart';
@@ -392,4 +393,48 @@
/// Finds the option whose name or alias matches [name], or `null` if no
/// option has that name or alias.
Option? findByNameOrAlias(String name) => options[_aliases[name] ?? name];
+
+ Map<String, Object?> get jsonSchema {
+ final properties = <String, Schema>{};
+ final required = <String>[];
+ for (final option in _options.values) {
+ if (option.hide) continue;
+ var help = option.help;
+ final extras = <String>[];
+ if (option.defaultsTo != null) {
+ extras.add('defaults to "${option.defaultsTo}"');
+ }
+ if (option.allowed?.isNotEmpty ?? false) {
+ extras.add('allowed values: ${option.allowed?.join(', ')}');
+ }
+ if (extras.isNotEmpty) {
+ help = [
+ if (help != null) help,
+ ...extras,
+ ].join('\n');
+ }
+ final schema = switch (option.type) {
+ OptionType.flag => Schema.bool(
+ description: help,
+ ),
+ OptionType.single => Schema.string(
+ description: help,
+ ),
+ OptionType.multiple => Schema.list(
+ description: help,
+ items: Schema.string(),
+ ),
+ _ => throw StateError('Unhandled Option Type: ${option.type.name}')
+ };
+
+ if (option.mandatory) {
+ required.add(option.name);
+ }
+ properties[option.name] = schema;
+ }
+ return Schema.object(
+ properties: properties,
+ required: required,
+ ).asMap();
+ }
}
diff --git a/pkgs/args/lib/src/json_schema.dart b/pkgs/args/lib/src/json_schema.dart
new file mode 100644
index 0000000..d62bed9
--- /dev/null
+++ b/pkgs/args/lib/src/json_schema.dart
@@ -0,0 +1,611 @@
+// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Copied from https://github.com/dart-lang/ai/blob/failures-only/pkgs/dart_mcp/lib/src/api/tools.dart
+
+/// The valid types for properties in a JSON-RCP2 schema.
+enum JsonType {
+ object('object'),
+ list('array'),
+ string('string'),
+ num('number'),
+ int('integer'),
+ bool('boolean'),
+ nil('null');
+
+ const JsonType(this.typeName);
+
+ final String typeName;
+}
+
+/// A JSON Schema object defining the any kind of property.
+///
+/// See the subtypes [ObjectSchema], [ListSchema], [StringSchema],
+/// [NumberSchema], [IntegerSchema], [BooleanSchema], [NullSchema].
+///
+/// To get an instance of a subtype, you should inspect the [type] as well as
+/// check for any schema combinators ([allOf], [anyOf], [oneOf], [not]), as both
+/// may be present.
+///
+/// If a [type] is provided, it applies to all sub-schemas, and you can cast all
+/// the sub-schemas directly to the specified type from the parent schema.
+///
+/// See https://json-schema.org/understanding-json-schema/reference for the full
+/// specification.
+///
+/// **Note:** Only a subset of the json schema spec is supported by these types,
+/// if you need something more complex you can create your own
+/// `Map<String, Object?>` and cast it to [Schema] (or [ObjectSchema]) directly.
+extension type Schema.fromMap(Map<String, Object?> _value) {
+ /// A combined schema, see
+ /// https://json-schema.org/understanding-json-schema/reference/combining#schema-composition
+ factory Schema.combined({
+ JsonType? type,
+ String? title,
+ String? description,
+ List<Schema>? allOf,
+ List<Schema>? anyOf,
+ List<Schema>? oneOf,
+ List<Schema>? not,
+ }) =>
+ Schema.fromMap({
+ if (type != null) 'type': type.typeName,
+ if (title != null) 'title': title,
+ if (description != null) 'description': description,
+ if (allOf != null) 'allOf': allOf,
+ if (anyOf != null) 'anyOf': anyOf,
+ if (oneOf != null) 'oneOf': oneOf,
+ if (not != null) 'not': not,
+ });
+
+ /// Alias for [StringSchema.new].
+ static const string = StringSchema.new;
+
+ /// Alias for [BooleanSchema.new].
+ static const bool = BooleanSchema.new;
+
+ /// Alias for [NumberSchema.new].
+ static const num = NumberSchema.new;
+
+ /// Alias for [IntegerSchema.new].
+ static const int = IntegerSchema.new;
+
+ /// Alias for [ListSchema.new].
+ static const list = ListSchema.new;
+
+ /// Alias for [ObjectSchema.new].
+ static const object = ObjectSchema.new;
+
+ /// Alias for [NullSchema.new].
+ static const nil = NullSchema.new;
+
+ /// The [JsonType] of this schema, if present.
+ ///
+ /// Use this in switch statements to determine the type of schema and cast to
+ /// the appropriate subtype.
+ ///
+ /// Note that it is good practice to include a default case, to avoid breakage
+ /// in the case that a new type is added.
+ ///
+ /// This is not required, and commonly won't be present if one of the schema
+ /// combinators ([allOf], [anyOf], [oneOf], or [not]) are used.
+ JsonType? get type => JsonType.values
+ .where(
+ (t) => (_value['type'] as String? ?? '') == t.typeName,
+ )
+ .firstOrNull;
+
+ /// A title for this schema, should be short.
+ String? get title => _value['title'] as String?;
+
+ /// A description of this schema.
+ String? get description => _value['description'] as String?;
+
+ /// Schema combinator that requires all sub-schemas to match.
+ List<Schema>? get allOf => (_value['allOf'] as List?)?.cast<Schema>();
+
+ /// Schema combinator that requires at least one of the sub-schemas to match.
+ List<Schema>? get anyOf => (_value['anyOf'] as List?)?.cast<Schema>();
+
+ /// Schema combinator that requires exactly one of the sub-schemas to match.
+ List<Schema>? get oneOf => (_value['oneOf'] as List?)?.cast<Schema>();
+
+ /// Schema combinator that requires none of the sub-schemas to match.
+ List<Schema>? get not => (_value['not'] as List?)?.cast<Schema>();
+
+ Map<String, Object?> asMap() => _value;
+}
+
+/// A JSON Schema definition for an object with properties.
+///
+/// `ObjectSchema` is used to define the expected structure, data types, and
+/// constraints for MCP argument objects. It allows you to specify:
+///
+/// - Which properties an object can or must have ([properties], [required]).
+/// - The schema for each of those properties (e.g., string, number, nested
+/// object).
+/// - Whether additional properties not explicitly defined are allowed
+/// ([additionalProperties], [unevaluatedProperties]).
+/// - Constraints on the number of properties ([minProperties],
+/// [maxProperties]).
+/// - Constraints on property names ([propertyNames]).
+///
+/// See https://json-schema.org/understanding-json-schema/reference/object.html
+/// for more details on object schemas.
+///
+/// Example:
+///
+/// To define a schema for a product object that requires `productId` and
+/// `productName`, has an optional `price` (non-negative number) and optional
+/// `tags` (list of unique strings), and optional `dimensions` (an object with
+/// required numeric length, width, and height):
+///
+/// ```dart
+/// final productSchema = ObjectSchema(
+/// title: 'Product',
+/// description: 'Schema for a product object',
+/// required: ['productId', 'productName'],
+/// properties: {
+/// 'productId': Schema.string(
+/// description: 'Unique identifier for the product',
+/// ),
+/// 'productName': Schema.string(description: 'Name of the product'),
+/// 'price': Schema.num(
+/// description: 'Price of the product',
+/// minimum: 0,
+/// ),
+/// 'tags': Schema.list(
+/// description: 'Optional list of tags for the product',
+/// items: Schema.string(),
+/// uniqueItems: true,
+/// ),
+/// 'dimensions': ObjectSchema(
+/// description: 'Optional product dimensions',
+/// properties: {
+/// 'length': Schema.num(),
+/// 'width': Schema.num(),
+/// 'height': Schema.num(),
+/// },
+/// required: ['length', 'width', 'height'],
+/// ),
+/// },
+/// additionalProperties: false, // No other properties allowed beyond those defined
+/// );
+/// ```
+///
+/// This schema can then be used with the `validate` method to check if a given
+/// JSON-like map conforms to the defined structure.
+///
+/// For example, valid data might look like:
+///
+/// ```json
+/// {
+/// "productId": "ABC12345",
+/// "productName": "Super Widget",
+/// "price": 19.99,
+/// "tags": ["electronics", "gadget"],
+/// "dimensions": {"length": 10, "width": 5, "height": 2.5}
+/// }
+/// ```
+///
+/// And invalid data (e.g., missing productName, or an extra undefined
+/// property):
+/// ```json
+/// {
+/// "productId": "XYZ67890",
+/// "price": 9.99
+/// }
+/// ```
+///
+/// ```json
+/// {
+/// "productId": "DEF4567",
+/// "productName": "Another Gadget",
+/// "color": "blue" // Invalid if additionalProperties is false
+/// }
+/// ```
+extension type ObjectSchema.fromMap(Map<String, Object?> _value)
+ implements Schema {
+ factory ObjectSchema({
+ String? title,
+ String? description,
+ Map<String, Schema>? properties,
+ Map<String, Schema>? patternProperties,
+ List<String>? required,
+
+ /// Must be one of bool, Schema, or Null
+ Object? additionalProperties,
+ bool? unevaluatedProperties,
+ StringSchema? propertyNames,
+ int? minProperties,
+ int? maxProperties,
+ }) =>
+ ObjectSchema.fromMap({
+ 'type': JsonType.object.typeName,
+ if (title != null) 'title': title,
+ if (description != null) 'description': description,
+ if (properties != null) 'properties': properties,
+ if (patternProperties != null) 'patternProperties': patternProperties,
+ if (required != null) 'required': required,
+ if (additionalProperties != null)
+ 'additionalProperties': additionalProperties,
+ if (unevaluatedProperties != null)
+ 'unevaluatedProperties': unevaluatedProperties,
+ if (propertyNames != null) 'propertyNames': propertyNames,
+ if (minProperties != null) 'minProperties': minProperties,
+ if (maxProperties != null) 'maxProperties': maxProperties,
+ });
+
+ /// A map of the properties of the object to the nested [Schema]s for those
+ /// properties.
+ Map<String, Schema>? get properties =>
+ (_value['properties'] as Map?)?.cast<String, Schema>();
+
+ /// A map of the property patterns of the object to the nested [Schema]s for
+ /// those properties.
+ ///
+ /// For example, to define a schema where any property name starting with
+ /// "x-" should have a string value:
+ ///
+ /// ```dart
+ /// final schema = ObjectSchema(patternProperties: {r'^x-': Schema.string()});
+ /// ```
+ ///
+ Map<String, Schema>? get patternProperties =>
+ (_value['patternProperties'] as Map?)?.cast<String, Schema>();
+
+ /// A list of the required properties by name.
+ ///
+ /// For example, to define a schema for an object that requires a `name`
+ /// property:
+ ///
+ /// ```dart
+ /// final schema = ObjectSchema(
+ /// required: ['name'],
+ /// properties: {'name': Schema.string()},
+ /// );
+ /// ```
+ ///
+ /// In this schema, an object like `{'name': 'John'}` would be valid, but
+ /// `{}` or `{'age': 30}` would be invalid because they do not contain the
+ /// required `name` property. Note that the type of the `name` property is
+ /// also defined using the `properties` field; `required` only enforces the
+ /// presence of the property, not its type or value, which are handled by
+ /// the corresponding schema in the `properties` map (if provided, otherwise
+ /// any value is accepted).
+ ///
+ /// Properties in this list must be set in the object.
+ List<String>? get required => (_value['required'] as List?)?.cast<String>();
+
+ /// Rules for additional properties that don't match the
+ /// [properties] or [patternProperties] schemas.
+ ///
+ /// Can be either a [bool] or a [Schema], if it is a [Schema] then additional
+ /// properties should match that [Schema].
+ ///
+ /// For example, to define a schema where any property not explicitly defined
+ /// in `properties` should have an integer value:
+ ///
+ /// ```dart
+ /// final schema = ObjectSchema(
+ /// properties: {'name': Schema.string()},
+ /// additionalProperties: Schema.int(),
+ /// );
+ /// ```
+ ///
+ /// In this schema, an object like `{'name': 'John', 'age': 30}` would be
+ /// valid, but `{'name': 'John', 'age': 'thirty'}` would be invalid because
+ /// `age` is not a defined property and its value is not an integer as
+ /// required by `additionalProperties`.
+ Object? get additionalProperties => _value['additionalProperties'];
+
+ /// Similar to [additionalProperties] but more flexible, see
+ /// https://json-schema.org/understanding-json-schema/reference/object#unevaluatedproperties
+ /// for more details.
+ ///
+ /// For example, to define a schema where any property not explicitly defined
+ /// in `properties` or matched by `patternProperties` is disallowed:
+ ///
+ /// ```dart
+ /// final schema = ObjectSchema(
+ /// properties: {'name': Schema.string()},
+ /// patternProperties: {r'^x-': Schema.string()},
+ /// unevaluatedProperties: false,
+ /// );
+ /// ```
+ ///
+ /// In this schema, an object like `{'name': 'John', 'x-id': '123'}` would be
+ /// valid, but `{'name': 'John', 'age': 30}` would be invalid because `age` is
+ /// neither a defined property nor matches the pattern, and
+ /// `unevaluatedProperties` is set to `false`.
+ bool? get unevaluatedProperties => _value['unevaluatedProperties'] as bool?;
+
+ /// A list of valid patterns for all property names.
+ ///
+ /// For example, to define a schema where all property names must start with
+ /// a lowercase letter:
+ ///
+ /// ```dart
+ /// final schema = ObjectSchema(
+ /// propertyNames: Schema.string(pattern: r'^[a-z].*$'),
+ /// );
+ /// ```
+ ///
+ /// In this schema, an object like `{'name': 'John', 'age': 30}` would be
+ /// valid, but `{'Name': 'John', 'Age': 30}` would be invalid because the
+ /// property names do not start with a lowercase letter.
+ StringSchema? get propertyNames =>
+ (_value['propertyNames'] as Map?)?.cast<String, Object?>()
+ as StringSchema?;
+
+ /// The minimum number of properties in this object.
+ ///
+ /// If the object has less than this many properties, it will be invalid.
+ int? get minProperties => _value['minProperties'] as int?;
+
+ /// The maximum number of properties in this object.
+ ///
+ /// If the object has more than this many properties, it will be invalid.
+ int? get maxProperties => _value['maxProperties'] as int?;
+}
+
+/// A JSON Schema definition for a String.
+extension type const StringSchema.fromMap(Map<String, Object?> _value)
+ implements Schema {
+ factory StringSchema({
+ String? title,
+ String? description,
+ int? minLength,
+ int? maxLength,
+ String? pattern,
+ }) =>
+ StringSchema.fromMap({
+ 'type': JsonType.string.typeName,
+ if (title != null) 'title': title,
+ if (description != null) 'description': description,
+ if (minLength != null) 'minLength': minLength,
+ if (maxLength != null) 'maxLength': maxLength,
+ if (pattern != null) 'pattern': pattern,
+ });
+
+ /// The minimum allowed length of this String.
+ int? get minLength => _value['minLength'] as int?;
+
+ /// The maximum allowed length of this String.
+ int? get maxLength => _value['maxLength'] as int?;
+
+ /// A regular expression pattern that the String must match.
+ String? get pattern => _value['pattern'] as String?;
+}
+
+/// A JSON Schema definition for a [num].
+extension type NumberSchema.fromMap(Map<String, Object?> _value)
+ implements Schema {
+ factory NumberSchema({
+ String? title,
+ String? description,
+ num? minimum,
+ num? maximum,
+ num? exclusiveMinimum,
+ num? exclusiveMaximum,
+ num? multipleOf,
+ }) =>
+ NumberSchema.fromMap({
+ 'type': JsonType.num.typeName,
+ if (title != null) 'title': title,
+ if (description != null) 'description': description,
+ if (minimum != null) 'minimum': minimum,
+ if (maximum != null) 'maximum': maximum,
+ if (exclusiveMinimum != null) 'exclusiveMinimum': exclusiveMinimum,
+ if (exclusiveMaximum != null) 'exclusiveMaximum': exclusiveMaximum,
+ if (multipleOf != null) 'multipleOf': multipleOf,
+ });
+
+ /// The minimum value (inclusive) for this number.
+ num? get minimum => _value['minimum'] as num?;
+
+ /// The maximum value (inclusive) for this number.
+ num? get maximum => _value['maximum'] as num?;
+
+ /// The minimum value (exclusive) for this number.
+ num? get exclusiveMinimum => _value['exclusiveMinimum'] as num?;
+
+ /// The maximum value (exclusive) for this number.
+ num? get exclusiveMaximum => _value['exclusiveMaximum'] as num?;
+
+ /// The value must be a multiple of this number.
+ num? get multipleOf => _value['multipleOf'] as num?;
+}
+
+/// A JSON Schema definition for an [int].
+extension type IntegerSchema.fromMap(Map<String, Object?> _value)
+ implements Schema {
+ factory IntegerSchema({
+ String? title,
+ String? description,
+ int? minimum,
+ int? maximum,
+ int? exclusiveMinimum,
+ int? exclusiveMaximum,
+ num? multipleOf,
+ }) =>
+ IntegerSchema.fromMap({
+ 'type': JsonType.int.typeName,
+ if (title != null) 'title': title,
+ if (description != null) 'description': description,
+ if (minimum != null) 'minimum': minimum,
+ if (maximum != null) 'maximum': maximum,
+ if (exclusiveMinimum != null) 'exclusiveMinimum': exclusiveMinimum,
+ if (exclusiveMaximum != null) 'exclusiveMaximum': exclusiveMaximum,
+ if (multipleOf != null) 'multipleOf': multipleOf,
+ });
+
+ /// The minimum value (inclusive) for this integer.
+ int? get minimum => _value['minimum'] as int?;
+
+ /// The maximum value (inclusive) for this integer.
+ int? get maximum => _value['maximum'] as int?;
+
+ /// The minimum value (exclusive) for this integer.
+ int? get exclusiveMinimum => _value['exclusiveMinimum'] as int?;
+
+ /// The maximum value (exclusive) for this integer.
+ int? get exclusiveMaximum => _value['exclusiveMaximum'] as int?;
+
+ /// The value must be a multiple of this number.
+ num? get multipleOf => _value['multipleOf'] as num?;
+}
+
+/// A JSON Schema definition for a [bool].
+extension type BooleanSchema.fromMap(Map<String, Object?> _value)
+ implements Schema {
+ factory BooleanSchema({String? title, String? description}) =>
+ BooleanSchema.fromMap({
+ 'type': JsonType.bool.typeName,
+ if (title != null) 'title': title,
+ if (description != null) 'description': description,
+ });
+}
+
+/// A JSON Schema definition for `null`.
+extension type NullSchema.fromMap(Map<String, Object?> _value)
+ implements Schema {
+ factory NullSchema({String? title, String? description}) =>
+ NullSchema.fromMap({
+ 'type': JsonType.nil.typeName,
+ if (title != null) 'title': title,
+ if (description != null) 'description': description,
+ });
+}
+
+/// A JSON Schema definition for a [List].
+extension type ListSchema.fromMap(Map<String, Object?> _value)
+ implements Schema {
+ factory ListSchema({
+ String? title,
+ String? description,
+ Schema? items,
+ List<Schema>? prefixItems,
+ bool? unevaluatedItems,
+ int? minItems,
+ int? maxItems,
+ bool? uniqueItems,
+ }) =>
+ ListSchema.fromMap({
+ 'type': JsonType.list.typeName,
+ if (title != null) 'title': title,
+ if (description != null) 'description': description,
+ if (items != null) 'items': items,
+ if (prefixItems != null) 'prefixItems': prefixItems,
+ if (unevaluatedItems != null) 'unevaluatedItems': unevaluatedItems,
+ if (minItems != null) 'minItems': minItems,
+ if (maxItems != null) 'maxItems': maxItems,
+ if (uniqueItems != null) 'uniqueItems': uniqueItems,
+ });
+
+ /// The schema for all the items in this list, or all those after
+ /// [prefixItems] (if present).
+ ///
+ /// For example, to define a schema for a list where all items must be
+ /// strings:
+ ///
+ /// ```dart
+ /// final schema = ListSchema(items: Schema.string());
+ /// ```
+ ///
+ /// In this schema, a list like `['apple', 'banana', 'cherry']` would be
+ /// valid, but `['apple', 42, 'cherry']` would be invalid because it
+ /// contains a non-string item.
+ ///
+ /// Note that if you want to define a schema for a list where the initial
+ /// items have specific types and the remaining items follow a different
+ /// schema, you can use the `prefixItems` property in conjunction with
+ /// `items`. For example, to allow a string followed by an integer, and
+ /// then any number of booleans:
+ ///
+ /// ```dart
+ /// final schema = ListSchema(
+ /// prefixItems: [Schema.string(), Schema.int()],
+ /// items: Schema.bool(),
+ /// );
+ /// ```
+ Schema? get items => _value['items'] as Schema?;
+
+ /// The schema for the initial items in this list, if specified.
+ ///
+ /// For example, to define a schema for a list where the first item must be
+ /// a string and the second item must be an integer:
+ ///
+ /// ```dart
+ /// final schema = ListSchema(
+ /// prefixItems: [Schema.string(), Schema.int()],
+ /// );
+ /// ```
+ ///
+ /// In this schema, a list like `['hello', 42]` would be valid, but
+ /// `[42, 'hello']` or `['hello']` would be invalid because they do not
+ /// conform to the specified order and types of the prefix items.
+ ///
+ /// Note that if you want to allow additional items in the list that do not
+ /// match the prefix items, you can use the `items` property to define a
+ /// schema for those additional items. For example, to allow any number of
+ /// additional strings after the initial string and integer:
+ ///
+ /// ```dart
+ /// final schema = ListSchema(
+ /// prefixItems: [Schema.string(), Schema.int()],
+ /// items: Schema.string()
+ /// );
+ /// ```
+ List<Schema>? get prefixItems =>
+ (_value['prefixItems'] as List?)?.cast<Schema>();
+
+ /// Whether or not additional items in the list are allowed that don't
+ /// match the [items] or [prefixItems] schemas.
+ ///
+ /// For example, to define a schema for a list where only items matching
+ /// `prefixItems` or `items` are allowed:
+ ///
+ /// ```dart
+ /// final schema = ListSchema(
+ /// prefixItems: [Schema.string(), Schema.int()],
+ /// items: Schema.bool(),
+ /// unevaluatedItems: false,
+ /// );
+ /// ```
+ ///
+ /// In this schema, a list like `['hello', 42, true]` would be valid, but
+ /// `['hello', 42, true, 123]` would be invalid because the last item does
+ /// not match the schema defined by `items` (which applies to items
+ /// beyond those covered by `prefixItems`), and `unevaluatedItems` is set
+ /// to `false`, disallowing any items not explicitly matched by the
+ /// schema.
+ bool? get unevaluatedItems => _value['unevaluatedItems'] as bool?;
+
+ /// The minimum number of items in this list.
+ int? get minItems => _value['minItems'] as int?;
+
+ /// The maximum number of items in this list.
+ int? get maxItems => _value['maxItems'] as int?;
+
+ /// Whether or not all the items in this list must be unique.
+ ///
+ /// For example, to define a schema for a list where all items must be
+ /// unique:
+ ///
+ /// ```dart
+ /// final schema = ListSchema(
+ /// items: Schema.string(),
+ /// uniqueItems: true,
+ /// );
+ /// ```
+ ///
+ /// In this schema, a list like `['apple', 'banana', 'cherry']` would be
+ /// valid, but `['apple', 'banana', 'apple']` would be invalid because it
+ /// contains duplicate items. Note that the type of the items is also
+ /// defined using the `items` property; `uniqueItems` only enforces the
+ /// uniqueness of the items, not their type or value, which are handled by
+ /// the corresponding schema in the `items` property.
+ bool? get uniqueItems => _value['uniqueItems'] as bool?;
+}
diff --git a/pkgs/args/test/json_schema_test.dart b/pkgs/args/test/json_schema_test.dart
new file mode 100644
index 0000000..aed4fc6
--- /dev/null
+++ b/pkgs/args/test/json_schema_test.dart
@@ -0,0 +1,83 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:args/args.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('ArgParser.jsonSchema', () {
+ test('returns a schema for a flag and an option', () {
+ final parser = ArgParser()
+ ..addFlag('foo', help: 'a foo flag')
+ ..addOption('bar', help: 'a bar option');
+ expect(parser.jsonSchema, {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'boolean',
+ 'description': 'a foo flag\n' 'defaults to "false"',
+ },
+ 'bar': {
+ 'type': 'string',
+ 'description': 'a bar option',
+ },
+ },
+ 'required': <String>[],
+ });
+ });
+
+ test('a multi option', () {
+ final parser = ArgParser()
+ ..addMultiOption('foo', help: 'a foo option', defaultsTo: ['a', 'b']);
+ expect(parser.jsonSchema, {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'array',
+ 'description': 'a foo option\ndefaults to "[a, b]"',
+ 'items': {'type': 'string'},
+ },
+ },
+ 'required': <String>[],
+ });
+ });
+
+ test('a mandatory option', () {
+ final parser = ArgParser()..addOption('foo', mandatory: true);
+ expect(parser.jsonSchema, {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'string',
+ },
+ },
+ 'required': ['foo'],
+ });
+ });
+
+ test('an option with allowed values', () {
+ final parser = ArgParser()
+ ..addOption('foo', allowed: ['a', 'b'], help: 'a foo option');
+ expect(parser.jsonSchema, {
+ 'type': 'object',
+ 'properties': {
+ 'foo': {
+ 'type': 'string',
+ 'description': 'a foo option\nallowed values: a, b',
+ },
+ },
+ 'required': <String>[],
+ });
+ });
+
+ test('a hidden option is not included', () {
+ final parser = ArgParser()..addOption('foo', hide: true);
+ expect(parser.jsonSchema, {
+ 'type': 'object',
+ 'properties': <String, Object?>{},
+ 'required': <String>[],
+ });
+ });
+ });
+}