Skip to content

Commit f04bd50

Browse files
Fix mapping of MinLength/MaxLength/Length attribute mapping in nullable string properties. (#6812)
1 parent b8caf95 commit f04bd50

File tree

2 files changed

+89
-9
lines changed

2 files changed

+89
-9
lines changed

src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Schema.Create.cs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ void ApplyDataAnnotations(ref JsonNode schema, AIJsonSchemaCreateContext ctx)
427427
if (ResolveAttribute<MinLengthAttribute>() is { } minLengthAttribute)
428428
{
429429
JsonObject obj = ConvertSchemaToObject(ref schema);
430-
if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue<string>() is "string")
430+
if (TryGetSchemaType(obj, out string? schemaType, out _) && schemaType is "string")
431431
{
432432
obj[MinLengthStringPropertyName] ??= minLengthAttribute.Length;
433433
}
@@ -440,7 +440,7 @@ void ApplyDataAnnotations(ref JsonNode schema, AIJsonSchemaCreateContext ctx)
440440
if (ResolveAttribute<MaxLengthAttribute>() is { } maxLengthAttribute)
441441
{
442442
JsonObject obj = ConvertSchemaToObject(ref schema);
443-
if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue<string>() is "string")
443+
if (TryGetSchemaType(obj, out string? schemaType, out _) && schemaType is "string")
444444
{
445445
obj[MaxLengthStringPropertyName] ??= maxLengthAttribute.Length;
446446
}
@@ -530,7 +530,7 @@ void ApplyDataAnnotations(ref JsonNode schema, AIJsonSchemaCreateContext ctx)
530530
{
531531
JsonObject obj = ConvertSchemaToObject(ref schema);
532532

533-
if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue<string>() is "string")
533+
if (TryGetSchemaType(obj, out string? schemaType, out _) && schemaType is "string")
534534
{
535535
if (lengthAttribute.MinimumLength > 0)
536536
{
@@ -629,6 +629,58 @@ static JsonArray CreateJsonArray(object?[] values, JsonSerializerOptions seriali
629629
}
630630
}
631631
#endif
632+
#if NET || NETFRAMEWORK
633+
static bool TryGetSchemaType(JsonObject schema, [NotNullWhen(true)] out string? schemaType, out bool isNullable)
634+
{
635+
schemaType = null;
636+
isNullable = false;
637+
638+
if (!schema.TryGetPropertyValue(TypePropertyName, out JsonNode? typeNode))
639+
{
640+
return false;
641+
}
642+
643+
switch (typeNode?.GetValueKind())
644+
{
645+
case JsonValueKind.String:
646+
schemaType = typeNode.GetValue<string>();
647+
return true;
648+
649+
case JsonValueKind.Array:
650+
string? foundSchemaType = null;
651+
foreach (JsonNode? entry in (JsonArray)typeNode)
652+
{
653+
if (entry?.GetValueKind() is not JsonValueKind.String)
654+
{
655+
return false;
656+
}
657+
658+
string entryValue = entry.GetValue<string>();
659+
if (entryValue is "null")
660+
{
661+
isNullable = true;
662+
continue;
663+
}
664+
665+
if (foundSchemaType is null)
666+
{
667+
foundSchemaType = entryValue;
668+
}
669+
else if (foundSchemaType != entryValue)
670+
{
671+
return false;
672+
}
673+
}
674+
675+
schemaType = foundSchemaType;
676+
return schemaType is not null;
677+
678+
default:
679+
return false;
680+
}
681+
}
682+
#endif
683+
632684
TAttribute? ResolveAttribute<TAttribute>()
633685
where TAttribute : Attribute
634686
{

test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Utilities/AIJsonUtilitiesTests.cs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -670,22 +670,22 @@ public static void CreateJsonSchema_IncorporatesTypesAndAnnotations_Net()
670670
"string",
671671
"null"
672672
],
673-
"minItems": 5
673+
"minLength": 5
674674
},
675675
"MaxLengthProp": {
676676
"type": [
677677
"string",
678678
"null"
679679
],
680-
"maxItems": 50
680+
"maxLength": 50
681681
},
682682
"LengthProp": {
683683
"type": [
684684
"string",
685685
"null"
686686
],
687-
"minItems": 3,
688-
"maxItems": 10
687+
"minLength": 3,
688+
"maxLength": 10
689689
},
690690
"MinLengthArrayProp": {
691691
"type": [
@@ -848,14 +848,14 @@ public static void CreateJsonSchema_IncorporatesTypesAndAnnotations_NetFx()
848848
"string",
849849
"null"
850850
],
851-
"minItems": 5
851+
"minLength": 5
852852
},
853853
"MaxLengthProp": {
854854
"type": [
855855
"string",
856856
"null"
857857
],
858-
"maxItems": 50
858+
"maxLength": 50
859859
},
860860
"MinLengthArrayProp": {
861861
"type": [
@@ -972,6 +972,33 @@ private sealed class CreateJsonSchema_IncorporatesTypesAndAnnotations_Type
972972
#endif
973973
}
974974

975+
[Fact]
976+
public static void ClassWithNullableMaxLengthProperty_ReturnsExpectedSchema()
977+
{
978+
JsonElement expectedSchema = JsonDocument.Parse("""
979+
{
980+
"type": "object",
981+
"properties": {
982+
"Value": {
983+
"type": ["string", "null"],
984+
"maxLength": 24,
985+
"minLength": 10
986+
}
987+
}
988+
}
989+
""").RootElement;
990+
991+
JsonElement actualSchema = AIJsonUtilities.CreateJsonSchema(typeof(ClassWithNullableMaxLengthProperty), serializerOptions: JsonContext.Default.Options);
992+
AssertDeepEquals(expectedSchema, actualSchema);
993+
}
994+
995+
public class ClassWithNullableMaxLengthProperty
996+
{
997+
[MinLength(10)]
998+
[MaxLength(24)]
999+
public string? Value { get; set; }
1000+
}
1001+
9751002
[Fact]
9761003
public static void AddAIContentType_DerivedAIContent()
9771004
{
@@ -1362,6 +1389,7 @@ private class DerivedAIContent : AIContent
13621389
[JsonSerializable(typeof(MyPoco))]
13631390
[JsonSerializable(typeof(MyEnumValue?))]
13641391
[JsonSerializable(typeof(object[]))]
1392+
[JsonSerializable(typeof(ClassWithNullableMaxLengthProperty))]
13651393
private partial class JsonContext : JsonSerializerContext;
13661394

13671395
private static bool DeepEquals(JsonElement element1, JsonElement element2)

0 commit comments

Comments
 (0)