diff --git a/GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs b/GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs
index 059d61aa..488c3427 100644
--- a/GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs
+++ b/GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs
@@ -510,6 +510,54 @@ public class YamlConfigLoaderTests
});
}
+ ///
+ /// 验证数值不满足 multipleOf 时会在运行时被拒绝。
+ ///
+ [Test]
+ public void LoadAsync_Should_Throw_When_Number_Violates_MultipleOf()
+ {
+ CreateConfigFile(
+ "monster/slime.yaml",
+ """
+ id: 1
+ name: Slime
+ hp: 12
+ """);
+ CreateSchemaFile(
+ "schemas/monster.schema.json",
+ """
+ {
+ "type": "object",
+ "required": ["id", "name", "hp"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "hp": {
+ "type": "integer",
+ "multipleOf": 5
+ }
+ }
+ }
+ """);
+
+ var loader = new YamlConfigLoader(_rootPath)
+ .RegisterTable("monster", "monster", "schemas/monster.schema.json",
+ static config => config.Id);
+ var registry = new ConfigRegistry();
+
+ var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry));
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(exception, Is.Not.Null);
+ Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.ConstraintViolation));
+ Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("hp"));
+ Assert.That(exception.Diagnostic.RawValue, Is.EqualTo("12"));
+ Assert.That(exception.Message, Does.Contain("multiple of 5"));
+ Assert.That(registry.Count, Is.EqualTo(0));
+ });
+ }
+
///
/// 验证字符串最小长度与最大长度约束会在运行时被统一拒绝。
///
@@ -762,6 +810,60 @@ public class YamlConfigLoaderTests
});
}
+ ///
+ /// 验证数组声明 uniqueItems 后,重复元素会在运行时被拒绝。
+ ///
+ [Test]
+ public void LoadAsync_Should_Throw_When_Array_Violates_UniqueItems()
+ {
+ CreateConfigFile(
+ "monster/slime.yaml",
+ """
+ id: 1
+ name: Slime
+ dropRates:
+ - 5
+ - 10
+ - 5
+ """);
+ CreateSchemaFile(
+ "schemas/monster.schema.json",
+ """
+ {
+ "type": "object",
+ "required": ["id", "name", "dropRates"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "dropRates": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
+ }
+ """);
+
+ var loader = new YamlConfigLoader(_rootPath)
+ .RegisterTable("monster", "monster", "schemas/monster.schema.json",
+ static config => config.Id);
+ var registry = new ConfigRegistry();
+
+ var exception = Assert.ThrowsAsync(async () => await loader.LoadAsync(registry));
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(exception, Is.Not.Null);
+ Assert.That(exception!.Diagnostic.FailureKind, Is.EqualTo(ConfigLoadFailureKind.ConstraintViolation));
+ Assert.That(exception.Diagnostic.DisplayPath, Is.EqualTo("dropRates[2]"));
+ Assert.That(exception.Diagnostic.RawValue, Is.EqualTo("5"));
+ Assert.That(exception.Message, Does.Contain("unique array items"));
+ Assert.That(registry.Count, Is.EqualTo(0));
+ });
+ }
+
///
/// 验证启用 schema 校验后,未知字段不会再被静默忽略。
///
diff --git a/GFramework.Game/Config/YamlConfigSchemaValidator.cs b/GFramework.Game/Config/YamlConfigSchemaValidator.cs
index 381f321c..c1f85a3e 100644
--- a/GFramework.Game/Config/YamlConfigSchemaValidator.cs
+++ b/GFramework.Game/Config/YamlConfigSchemaValidator.cs
@@ -7,6 +7,8 @@ namespace GFramework.Game.Config;
/// 提供 YAML 配置文件与 JSON Schema 之间的最小运行时校验能力。
/// 该校验器与当前配置生成器、VS Code 工具支持的 schema 子集保持一致,
/// 并通过递归遍历方式覆盖嵌套对象、对象数组、标量数组与深层 enum / 引用约束。
+/// 当前共享子集额外支持 multipleOf 与 uniqueItems,
+/// 让数值步进和数组去重规则在运行时与生成器 / 工具侧保持一致。
///
internal static class YamlConfigSchemaValidator
{
@@ -603,6 +605,8 @@ internal static class YamlConfigSchemaValidator
schemaNode.ItemNode,
references);
}
+
+ ValidateArrayUniqueItemsConstraint(tableName, yamlPath, displayPath, sequenceNode, schemaNode);
}
///
@@ -776,6 +780,7 @@ internal static class YamlConfigSchemaValidator
TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "exclusiveMinimum");
var exclusiveMaximum =
TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "exclusiveMaximum");
+ var multipleOf = TryParseMultipleOfConstraint(tableName, schemaPath, propertyPath, element, nodeType);
var minLength = TryParseLengthConstraint(tableName, schemaPath, propertyPath, element, nodeType, "minLength");
var maxLength = TryParseLengthConstraint(tableName, schemaPath, propertyPath, element, nodeType, "maxLength");
var pattern = TryParsePatternConstraint(tableName, schemaPath, propertyPath, element, nodeType);
@@ -813,6 +818,7 @@ internal static class YamlConfigSchemaValidator
!maximum.HasValue &&
!exclusiveMinimum.HasValue &&
!exclusiveMaximum.HasValue &&
+ !multipleOf.HasValue &&
!minLength.HasValue &&
!maxLength.HasValue &&
pattern is null)
@@ -825,6 +831,7 @@ internal static class YamlConfigSchemaValidator
maximum,
exclusiveMinimum,
exclusiveMaximum,
+ multipleOf,
minLength,
maxLength,
pattern,
@@ -851,6 +858,7 @@ internal static class YamlConfigSchemaValidator
{
var minItems = TryParseArrayLengthConstraint(tableName, schemaPath, propertyPath, element, "minItems");
var maxItems = TryParseArrayLengthConstraint(tableName, schemaPath, propertyPath, element, "maxItems");
+ var uniqueItems = TryParseUniqueItemsConstraint(tableName, schemaPath, propertyPath, element);
if (minItems.HasValue && maxItems.HasValue && minItems.Value > maxItems.Value)
{
@@ -862,9 +870,9 @@ internal static class YamlConfigSchemaValidator
displayPath: GetDiagnosticPath(propertyPath));
}
- return !minItems.HasValue && !maxItems.HasValue
+ return !minItems.HasValue && !maxItems.HasValue && !uniqueItems
? null
- : new YamlConfigArrayConstraints(minItems, maxItems);
+ : new YamlConfigArrayConstraints(minItems, maxItems, uniqueItems);
}
///
@@ -917,6 +925,41 @@ internal static class YamlConfigSchemaValidator
return constraintValue;
}
+ ///
+ /// 读取 multipleOf 约束。
+ ///
+ /// 所属配置表名称。
+ /// Schema 文件路径。
+ /// 字段路径。
+ /// Schema 节点。
+ /// 字段类型。
+ /// 步进约束;未声明时返回空。
+ private static double? TryParseMultipleOfConstraint(
+ string tableName,
+ string schemaPath,
+ string propertyPath,
+ JsonElement element,
+ YamlConfigSchemaPropertyType nodeType)
+ {
+ var multipleOf = TryParseNumericConstraint(tableName, schemaPath, propertyPath, element, nodeType, "multipleOf");
+ if (!multipleOf.HasValue)
+ {
+ return null;
+ }
+
+ if (multipleOf.Value <= 0d)
+ {
+ throw ConfigLoadExceptionFactory.Create(
+ ConfigLoadFailureKind.SchemaUnsupported,
+ tableName,
+ $"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'multipleOf' as a positive finite number.",
+ schemaPath: schemaPath,
+ displayPath: GetDiagnosticPath(propertyPath));
+ }
+
+ return multipleOf;
+ }
+
///
/// 读取字符串长度约束。
///
@@ -1062,6 +1105,39 @@ internal static class YamlConfigSchemaValidator
return constraintValue;
}
+ ///
+ /// 读取数组去重约束。
+ ///
+ /// 所属配置表名称。
+ /// Schema 文件路径。
+ /// 字段路径。
+ /// Schema 节点。
+ /// 是否启用 uniqueItems。
+ private static bool TryParseUniqueItemsConstraint(
+ string tableName,
+ string schemaPath,
+ string propertyPath,
+ JsonElement element)
+ {
+ if (!element.TryGetProperty("uniqueItems", out var constraintElement))
+ {
+ return false;
+ }
+
+ if (constraintElement.ValueKind != JsonValueKind.True &&
+ constraintElement.ValueKind != JsonValueKind.False)
+ {
+ throw ConfigLoadExceptionFactory.Create(
+ ConfigLoadFailureKind.SchemaUnsupported,
+ tableName,
+ $"Property '{propertyPath}' in schema file '{schemaPath}' must declare 'uniqueItems' as a boolean.",
+ schemaPath: schemaPath,
+ displayPath: GetDiagnosticPath(propertyPath));
+ }
+
+ return constraintElement.GetBoolean();
+ }
+
///
/// 校验数值上下界组合不会形成空区间。
/// 这里把闭区间与开区间统一折算为最强边界,避免 schema 进入“无任何合法值”的状态。
@@ -1238,6 +1314,20 @@ internal static class YamlConfigSchemaValidator
$"Exclusive maximum allowed value: {constraints.ExclusiveMaximum.Value.ToString(CultureInfo.InvariantCulture)}.");
}
+ if (constraints.MultipleOf.HasValue &&
+ !IsMultipleOf(numericValue, constraints.MultipleOf.Value))
+ {
+ throw ConfigLoadExceptionFactory.Create(
+ ConfigLoadFailureKind.ConstraintViolation,
+ tableName,
+ $"Property '{displayPath}' in config file '{yamlPath}' must be a multiple of {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}, but the current YAML scalar value is '{rawValue}'.",
+ yamlPath: yamlPath,
+ schemaPath: schemaNode.SchemaPathHint,
+ displayPath: GetDiagnosticPath(displayPath),
+ rawValue: rawValue,
+ detail: $"Required numeric step: {constraints.MultipleOf.Value.ToString(CultureInfo.InvariantCulture)}.");
+ }
+
return;
case YamlConfigSchemaPropertyType.String:
@@ -1345,6 +1435,159 @@ internal static class YamlConfigSchemaValidator
}
}
+ ///
+ /// 校验数组是否满足去重约束。
+ ///
+ /// 所属配置表名称。
+ /// YAML 文件路径。
+ /// 字段路径。
+ /// 实际数组节点。
+ /// 数组 schema 节点。
+ private static void ValidateArrayUniqueItemsConstraint(
+ string tableName,
+ string yamlPath,
+ string displayPath,
+ YamlSequenceNode sequenceNode,
+ YamlConfigSchemaNode schemaNode)
+ {
+ var constraints = schemaNode.ArrayConstraints;
+ if (constraints is null ||
+ !constraints.UniqueItems ||
+ schemaNode.ItemNode is null)
+ {
+ return;
+ }
+
+ // The canonical item key uses schema-aware normalization so object key order,
+ // scalar quoting, and numeric formatting do not accidentally bypass uniqueItems.
+ Dictionary seenItems = new(StringComparer.Ordinal);
+ for (var itemIndex = 0; itemIndex < sequenceNode.Children.Count; itemIndex++)
+ {
+ var itemNode = sequenceNode.Children[itemIndex];
+ var comparableValue = BuildComparableNodeValue(itemNode, schemaNode.ItemNode);
+ if (seenItems.TryGetValue(comparableValue, out var existingIndex))
+ {
+ var itemPath = $"{displayPath}[{itemIndex}]";
+ throw ConfigLoadExceptionFactory.Create(
+ ConfigLoadFailureKind.ConstraintViolation,
+ tableName,
+ $"Property '{displayPath}' in config file '{yamlPath}' requires unique array items, but item '{itemPath}' duplicates '{displayPath}[{existingIndex}]'.",
+ yamlPath: yamlPath,
+ schemaPath: schemaNode.SchemaPathHint,
+ displayPath: itemPath,
+ rawValue: DescribeYamlNodeForDiagnostics(itemNode, schemaNode.ItemNode),
+ detail: "The schema declares uniqueItems = true.");
+ }
+
+ seenItems.Add(comparableValue, itemIndex);
+ }
+ }
+
+ ///
+ /// 将一个已通过结构校验的 YAML 节点归一化为可比较字符串。
+ /// 该键仅用于 uniqueItems,因此要忽略对象字段顺序和字符串引号形式。
+ ///
+ /// YAML 节点。
+ /// 对应 schema 节点。
+ /// 可稳定比较的归一化键。
+ private static string BuildComparableNodeValue(YamlNode node, YamlConfigSchemaNode schemaNode)
+ {
+ switch (schemaNode.NodeType)
+ {
+ case YamlConfigSchemaPropertyType.Object:
+ if (node is not YamlMappingNode mappingNode)
+ {
+ throw new InvalidOperationException("Validated object nodes must be YAML mappings.");
+ }
+
+ var objectEntries = new List>(mappingNode.Children.Count);
+ foreach (var entry in mappingNode.Children)
+ {
+ if (entry.Key is not YamlScalarNode keyNode ||
+ keyNode.Value is null ||
+ schemaNode.Properties is null ||
+ !schemaNode.Properties.TryGetValue(keyNode.Value, out var propertySchema))
+ {
+ throw new InvalidOperationException("Validated object nodes must use declared scalar property names.");
+ }
+
+ objectEntries.Add(
+ new KeyValuePair(
+ keyNode.Value,
+ BuildComparableNodeValue(entry.Value, propertySchema)));
+ }
+
+ objectEntries.Sort(static (left, right) => string.CompareOrdinal(left.Key, right.Key));
+ return string.Join(
+ "|",
+ objectEntries.Select(static entry =>
+ $"{entry.Key.Length.ToString(CultureInfo.InvariantCulture)}:{entry.Key}={entry.Value.Length.ToString(CultureInfo.InvariantCulture)}:{entry.Value}"));
+
+ case YamlConfigSchemaPropertyType.Array:
+ if (node is not YamlSequenceNode sequenceNode ||
+ schemaNode.ItemNode is null)
+ {
+ throw new InvalidOperationException("Validated array nodes must be YAML sequences with item schema.");
+ }
+
+ return "[" +
+ string.Join(
+ ",",
+ sequenceNode.Children.Select(
+ item => BuildComparableNodeValue(item, schemaNode.ItemNode))) +
+ "]";
+
+ case YamlConfigSchemaPropertyType.Integer:
+ case YamlConfigSchemaPropertyType.Number:
+ case YamlConfigSchemaPropertyType.Boolean:
+ case YamlConfigSchemaPropertyType.String:
+ if (node is not YamlScalarNode scalarNode ||
+ scalarNode.Value is null)
+ {
+ throw new InvalidOperationException("Validated scalar nodes must be YAML scalars.");
+ }
+
+ var normalizedScalar = NormalizeScalarValue(schemaNode.NodeType, scalarNode.Value);
+ return $"{schemaNode.NodeType}:{normalizedScalar.Length.ToString(CultureInfo.InvariantCulture)}:{normalizedScalar}";
+
+ default:
+ throw new InvalidOperationException($"Unsupported schema node type '{schemaNode.NodeType}'.");
+ }
+ }
+
+ ///
+ /// 为唯一性诊断提取一个可读的节点摘要。
+ ///
+ /// YAML 节点。
+ /// 对应 schema 节点。
+ /// 诊断摘要。
+ private static string DescribeYamlNodeForDiagnostics(YamlNode node, YamlConfigSchemaNode schemaNode)
+ {
+ return schemaNode.NodeType switch
+ {
+ YamlConfigSchemaPropertyType.Object => "{...}",
+ YamlConfigSchemaPropertyType.Array => "[...]",
+ _ when node is YamlScalarNode scalarNode => scalarNode.Value ?? string.Empty,
+ _ => node.GetType().Name
+ };
+ }
+
+ ///
+ /// 判断数值是否满足 multipleOf。
+ /// 双精度浮点比较会保留一个与商值量级相关的微小容差,
+ /// 以避免运行时与 JS 工具侧在 0.1 / 0.01 这类十进制步进上出现伪失败。
+ ///
+ /// 当前值。
+ /// 步进约束。
+ /// 是否满足整倍数关系。
+ private static bool IsMultipleOf(double value, double divisor)
+ {
+ var quotient = value / divisor;
+ var nearestInteger = Math.Round(quotient);
+ var tolerance = 1e-9 * Math.Max(1d, Math.Abs(quotient));
+ return Math.Abs(quotient - nearestInteger) <= tolerance;
+ }
+
///
/// 解析跨表引用目标表名称。
///
@@ -1758,7 +2001,7 @@ internal sealed class YamlConfigSchemaNode
}
///
-/// 表示一个标量节点上声明的数值范围或字符串长度约束。
+/// 表示一个标量节点上声明的数值范围、步进或字符串长度约束。
/// 该模型让运行时、热重载和跨文件诊断都能复用同一份最小约束信息。
///
internal sealed class YamlConfigScalarConstraints
@@ -1770,6 +2013,7 @@ internal sealed class YamlConfigScalarConstraints
/// 最大值约束。
/// 开区间最小值约束。
/// 开区间最大值约束。
+ /// 数值步进约束。
/// 最小长度约束。
/// 最大长度约束。
/// 正则模式约束。
@@ -1779,6 +2023,7 @@ internal sealed class YamlConfigScalarConstraints
double? maximum,
double? exclusiveMinimum,
double? exclusiveMaximum,
+ double? multipleOf,
int? minLength,
int? maxLength,
string? pattern,
@@ -1788,6 +2033,7 @@ internal sealed class YamlConfigScalarConstraints
Maximum = maximum;
ExclusiveMinimum = exclusiveMinimum;
ExclusiveMaximum = exclusiveMaximum;
+ MultipleOf = multipleOf;
MinLength = minLength;
MaxLength = maxLength;
Pattern = pattern;
@@ -1814,6 +2060,11 @@ internal sealed class YamlConfigScalarConstraints
///
public double? ExclusiveMaximum { get; }
+ ///
+ /// 获取数值步进约束。
+ ///
+ public double? MultipleOf { get; }
+
///
/// 获取最小长度约束。
///
@@ -1836,7 +2087,7 @@ internal sealed class YamlConfigScalarConstraints
}
///
-/// 表示一个数组节点上声明的元素数量约束。
+/// 表示一个数组节点上声明的元素数量或去重约束。
/// 该模型与标量约束拆分保存,避免数组节点继续共享不适用的标量字段。
///
internal sealed class YamlConfigArrayConstraints
@@ -1846,10 +2097,12 @@ internal sealed class YamlConfigArrayConstraints
///
/// 最小元素数量约束。
/// 最大元素数量约束。
- public YamlConfigArrayConstraints(int? minItems, int? maxItems)
+ /// 是否要求数组元素唯一。
+ public YamlConfigArrayConstraints(int? minItems, int? maxItems, bool uniqueItems)
{
MinItems = minItems;
MaxItems = maxItems;
+ UniqueItems = uniqueItems;
}
///
@@ -1861,6 +2114,11 @@ internal sealed class YamlConfigArrayConstraints
/// 获取最大元素数量约束。
///
public int? MaxItems { get; }
+
+ ///
+ /// 获取是否要求数组元素唯一。
+ ///
+ public bool UniqueItems { get; }
}
///
diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs
index 290985ee..dc2a36b5 100644
--- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs
+++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorSnapshotTests.cs
@@ -92,6 +92,7 @@ public class SchemaConfigGeneratorSnapshotTests
"maximum": 999,
"exclusiveMinimum": 0,
"exclusiveMaximum": 1000,
+ "multipleOf": 5,
"default": 10
},
"dropItems": {
@@ -99,6 +100,7 @@ public class SchemaConfigGeneratorSnapshotTests
"type": "array",
"minItems": 1,
"maxItems": 3,
+ "uniqueItems": true,
"items": {
"type": "string",
"minLength": 3,
diff --git a/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/MonsterConfig.g.txt b/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/MonsterConfig.g.txt
index 2306fa00..d5b5f909 100644
--- a/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/MonsterConfig.g.txt
+++ b/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/MonsterConfig.g.txt
@@ -34,7 +34,7 @@ public sealed partial class MonsterConfig
///
///
/// Schema property path: 'hp'.
- /// Constraints: minimum = 1, exclusiveMinimum = 0, maximum = 999, exclusiveMaximum = 1000.
+ /// Constraints: minimum = 1, exclusiveMinimum = 0, maximum = 999, exclusiveMaximum = 1000, multipleOf = 5.
/// Generated default initializer: = 10;
///
public int? Hp { get; set; } = 10;
@@ -45,7 +45,7 @@ public sealed partial class MonsterConfig
///
/// Schema property path: 'dropItems'.
/// Allowed values: potion, slime_gel.
- /// Constraints: minItems = 1, maxItems = 3.
+ /// Constraints: minItems = 1, maxItems = 3, uniqueItems = true.
/// References config table: 'item'.
/// Item constraints: minLength = 3, maxLength = 12.
/// Generated default initializer: = new string[] { "potion" };
diff --git a/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
index ca249109..5890cc1f 100644
--- a/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
+++ b/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs
@@ -6,6 +6,8 @@ namespace GFramework.SourceGenerators.Config;
/// 根据 AdditionalFiles 中的 JSON schema 生成配置类型和配置表包装。
/// 当前实现聚焦 AI-First 配置系统共享的最小 schema 子集,
/// 支持嵌套对象、对象数组、标量数组,以及可映射的 default / enum / ref-table 元数据。
+/// 当前共享子集也会把 multipleOf 与 uniqueItems 写入生成代码文档,
+/// 让消费者能直接在强类型 API 上看到运行时生效的约束。
///
[Generator]
public sealed class SchemaConfigGenerator : IIncrementalGenerator
@@ -2430,7 +2432,7 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
}
///
- /// 将 shared schema 子集中的范围、长度、模式与数组数量约束整理成 XML 文档可读字符串。
+ /// 将 shared schema 子集中的范围、步进、长度、模式与数组数量 / 去重约束整理成 XML 文档可读字符串。
///
/// Schema 节点。
/// 标量类型。
@@ -2463,6 +2465,13 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
parts.Add($"exclusiveMaximum = {exclusiveMaximum.ToString(CultureInfo.InvariantCulture)}");
}
+ if ((schemaType == "integer" || schemaType == "number") &&
+ TryGetFiniteNumber(element, "multipleOf", out var multipleOf) &&
+ multipleOf > 0d)
+ {
+ parts.Add($"multipleOf = {multipleOf.ToString(CultureInfo.InvariantCulture)}");
+ }
+
if (schemaType == "string" &&
TryGetNonNegativeInt32(element, "minLength", out var minLength))
{
@@ -2494,6 +2503,13 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
parts.Add($"maxItems = {maxItems.ToString(CultureInfo.InvariantCulture)}");
}
+ if (schemaType == "array" &&
+ element.TryGetProperty("uniqueItems", out var uniqueItemsElement) &&
+ uniqueItemsElement.ValueKind == JsonValueKind.True)
+ {
+ parts.Add("uniqueItems = true");
+ }
+
return parts.Count > 0 ? string.Join(", ", parts) : null;
}