Skip to content

Commit e838066

Browse files
committed
Augment AIJsonUtilities.CreateJsonSchema for more types and annotations
1 parent 3d2ddda commit e838066

File tree

4 files changed

+766
-42
lines changed

4 files changed

+766
-42
lines changed

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

Lines changed: 290 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
using System;
55
using System.ComponentModel;
6+
#if NET
7+
using System.ComponentModel.DataAnnotations;
8+
#endif
69
using System.Diagnostics;
710
using System.Diagnostics.CodeAnalysis;
811
using System.Reflection;
@@ -14,11 +17,11 @@
1417
using System.Threading;
1518
using Microsoft.Shared.Diagnostics;
1619

17-
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
1820
#pragma warning disable S107 // Methods should not have too many parameters
21+
#pragma warning disable S109 // Magic numbers should not be used
1922
#pragma warning disable S1075 // URIs should not be hardcoded
23+
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
2024
#pragma warning disable SA1118 // Parameter should not span multiple lines
21-
#pragma warning disable S109 // Magic numbers should not be used
2225

2326
namespace Microsoft.Extensions.AI;
2427

@@ -38,6 +41,19 @@ public static partial class AIJsonUtilities
3841
private const string AdditionalPropertiesPropertyName = "additionalProperties";
3942
private const string DefaultPropertyName = "default";
4043
private const string RefPropertyName = "$ref";
44+
private const string FormatPropertyName = "format";
45+
#if NET
46+
private const string ContentEncodingPropertyName = "contentEncoding";
47+
private const string ContentMediaTypePropertyName = "contentMediaType";
48+
private const string MinLengthStringPropertyName = "minLength";
49+
private const string MaxLengthStringPropertyName = "maxLength";
50+
private const string MinLengthCollectionPropertyName = "minItems";
51+
private const string MaxLengthCollectionPropertyName = "maxItems";
52+
private const string MinRangePropertyName = "minimum";
53+
private const string MaxRangePropertyName = "maximum";
54+
private const string MinExclusiveRangePropertyName = "exclusiveMinimum";
55+
private const string MaxExclusiveRangePropertyName = "exclusiveMaximum";
56+
#endif
4157

4258
/// <summary>The uri used when populating the $schema keyword in created schemas.</summary>
4359
private const string SchemaKeywordUri = "https://json-schema.org/draft/2020-12/schema";
@@ -318,6 +334,9 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js
318334
ConvertSchemaToObject(ref schema).InsertAtStart(SchemaPropertyName, (JsonNode)SchemaKeywordUri);
319335
}
320336

337+
ApplyDataTypeFormats(parameterName, ref schema, ctx);
338+
ApplyDataAnnotations(parameterName, ref schema, ctx);
339+
321340
// Finally, apply any user-defined transformations if specified.
322341
if (inferenceOptions.TransformSchemaNode is { } transformer)
323342
{
@@ -345,6 +364,275 @@ static JsonObject ConvertSchemaToObject(ref JsonNode schema)
345364
return obj;
346365
}
347366
}
367+
368+
static void ApplyDataTypeFormats(string? parameterName, ref JsonNode schema, AIJsonSchemaCreateContext ctx)
369+
{
370+
Type t = ctx.TypeInfo.Type;
371+
372+
if (Nullable.GetUnderlyingType(t) is { } underlyingType)
373+
{
374+
t = underlyingType;
375+
}
376+
377+
if (t == typeof(DateTime) || t == typeof(DateTimeOffset))
378+
{
379+
ConvertSchemaToObject(ref schema)[FormatPropertyName] = "date-time";
380+
}
381+
#if NET
382+
else if (t == typeof(DateOnly))
383+
{
384+
ConvertSchemaToObject(ref schema)[FormatPropertyName] = "date";
385+
}
386+
else if (t == typeof(TimeOnly))
387+
{
388+
ConvertSchemaToObject(ref schema)[FormatPropertyName] = "time";
389+
}
390+
#endif
391+
else if (t == typeof(TimeSpan))
392+
{
393+
ConvertSchemaToObject(ref schema)[FormatPropertyName] = "duration";
394+
}
395+
else if (t == typeof(Guid))
396+
{
397+
ConvertSchemaToObject(ref schema)[FormatPropertyName] = "uuid";
398+
}
399+
else if (t == typeof(Uri))
400+
{
401+
ConvertSchemaToObject(ref schema)[FormatPropertyName] = "uri";
402+
}
403+
}
404+
405+
void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSchemaCreateContext ctx)
406+
{
407+
if (ctx.GetCustomAttribute<DisplayNameAttribute>() is { } displayNameAttribute)
408+
{
409+
ConvertSchemaToObject(ref schema)[TitlePropertyName] ??= displayNameAttribute.DisplayName;
410+
}
411+
412+
#if NET
413+
if (ctx.GetCustomAttribute<Base64StringAttribute>() is { } base64Attribute)
414+
{
415+
ConvertSchemaToObject(ref schema)[ContentEncodingPropertyName] ??= "base64";
416+
}
417+
418+
if (ctx.GetCustomAttribute<EmailAddressAttribute>() is { } emailAttribute)
419+
{
420+
ConvertSchemaToObject(ref schema)[FormatPropertyName] ??= "email";
421+
}
422+
423+
if (ctx.GetCustomAttribute<UrlAttribute>() is { } urlAttribute)
424+
{
425+
ConvertSchemaToObject(ref schema)[FormatPropertyName] ??= "uri";
426+
}
427+
428+
if (ctx.GetCustomAttribute<RegularExpressionAttribute>() is { } regexAttribute)
429+
{
430+
ConvertSchemaToObject(ref schema)[PatternPropertyName] ??= regexAttribute.Pattern;
431+
}
432+
433+
if (ctx.GetCustomAttribute<StringLengthAttribute>() is { } stringLengthAttribute)
434+
{
435+
JsonObject obj = ConvertSchemaToObject(ref schema);
436+
437+
if (stringLengthAttribute.MinimumLength > 0)
438+
{
439+
obj[MinLengthStringPropertyName] ??= stringLengthAttribute.MinimumLength;
440+
}
441+
442+
obj[MaxLengthStringPropertyName] ??= stringLengthAttribute.MaximumLength;
443+
}
444+
445+
if (ctx.GetCustomAttribute<LengthAttribute>() is { } lengthAttribute)
446+
{
447+
JsonObject obj = ConvertSchemaToObject(ref schema);
448+
449+
if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue<string>() is "string")
450+
{
451+
if (lengthAttribute.MinimumLength > 0)
452+
{
453+
obj[MinLengthStringPropertyName] ??= lengthAttribute.MinimumLength;
454+
}
455+
456+
obj[MaxLengthStringPropertyName] ??= lengthAttribute.MaximumLength;
457+
}
458+
else
459+
{
460+
if (lengthAttribute.MinimumLength > 0)
461+
{
462+
obj[MinLengthCollectionPropertyName] ??= lengthAttribute.MinimumLength;
463+
}
464+
465+
obj[MaxLengthCollectionPropertyName] ??= lengthAttribute.MaximumLength;
466+
}
467+
}
468+
469+
if (ctx.GetCustomAttribute<MinLengthAttribute>() is { } minLengthAttribute)
470+
{
471+
JsonObject obj = ConvertSchemaToObject(ref schema);
472+
if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue<string>() is "string")
473+
{
474+
obj[MinLengthStringPropertyName] ??= minLengthAttribute.Length;
475+
}
476+
else
477+
{
478+
obj[MinLengthCollectionPropertyName] ??= minLengthAttribute.Length;
479+
}
480+
}
481+
482+
if (ctx.GetCustomAttribute<MaxLengthAttribute>() is { } maxLengthAttribute)
483+
{
484+
JsonObject obj = ConvertSchemaToObject(ref schema);
485+
if (obj[TypePropertyName] is JsonNode typeNode && typeNode.GetValueKind() is JsonValueKind.String && typeNode.GetValue<string>() is "string")
486+
{
487+
obj[MaxLengthStringPropertyName] ??= maxLengthAttribute.Length;
488+
}
489+
else
490+
{
491+
obj[MaxLengthCollectionPropertyName] ??= maxLengthAttribute.Length;
492+
}
493+
}
494+
495+
if (ctx.GetCustomAttribute<RangeAttribute>() is { } rangeAttribute)
496+
{
497+
JsonObject obj = ConvertSchemaToObject(ref schema);
498+
499+
JsonNode? minNode = null;
500+
JsonNode? maxNode = null;
501+
switch (rangeAttribute.Minimum)
502+
{
503+
case int minInt32 when rangeAttribute.Maximum is int maxInt32:
504+
maxNode = maxInt32;
505+
if (!rangeAttribute.MinimumIsExclusive || minInt32 > 0)
506+
{
507+
minNode = minInt32;
508+
}
509+
510+
break;
511+
512+
case double minDouble when rangeAttribute.Maximum is double maxDouble:
513+
maxNode = maxDouble;
514+
if (!rangeAttribute.MinimumIsExclusive || minDouble > 0)
515+
{
516+
minNode = minDouble;
517+
}
518+
519+
break;
520+
521+
case string minString when rangeAttribute.Maximum is string maxString:
522+
maxNode = maxString;
523+
minNode = minString;
524+
break;
525+
}
526+
527+
if (minNode is not null)
528+
{
529+
if (rangeAttribute.MinimumIsExclusive)
530+
{
531+
obj[MinExclusiveRangePropertyName] ??= minNode;
532+
}
533+
else
534+
{
535+
obj[MinRangePropertyName] ??= minNode;
536+
}
537+
}
538+
539+
if (maxNode is not null)
540+
{
541+
if (rangeAttribute.MaximumIsExclusive)
542+
{
543+
obj[MaxExclusiveRangePropertyName] ??= maxNode;
544+
}
545+
else
546+
{
547+
obj[MaxRangePropertyName] ??= maxNode;
548+
}
549+
}
550+
}
551+
552+
if (ctx.GetCustomAttribute<AllowedValuesAttribute>() is { } allowedValuesAttribute)
553+
{
554+
JsonObject obj = ConvertSchemaToObject(ref schema);
555+
if (!obj.ContainsKey(EnumPropertyName))
556+
{
557+
if (CreateJsonArray(allowedValuesAttribute.Values, serializerOptions) is { Count: > 0 } enumArray)
558+
{
559+
obj[EnumPropertyName] = enumArray;
560+
}
561+
}
562+
}
563+
564+
if (ctx.GetCustomAttribute<DeniedValuesAttribute>() is { } deniedValuesAttribute)
565+
{
566+
JsonObject obj = ConvertSchemaToObject(ref schema);
567+
568+
JsonNode? notNode = obj[NotPropertyName];
569+
if (notNode is null or JsonObject)
570+
{
571+
JsonObject notObj =
572+
notNode as JsonObject ??
573+
(JsonObject)(obj[NotPropertyName] = new JsonObject());
574+
575+
if (notObj[EnumPropertyName] is null)
576+
{
577+
if (CreateJsonArray(deniedValuesAttribute.Values, serializerOptions) is { Count: > 0 } enumArray)
578+
{
579+
notObj[EnumPropertyName] = enumArray;
580+
}
581+
}
582+
}
583+
}
584+
585+
static JsonArray CreateJsonArray(object?[] values, JsonSerializerOptions serializerOptions)
586+
{
587+
JsonArray enumArray = new();
588+
foreach (object? allowedValue in values)
589+
{
590+
if (allowedValue is not null && JsonSerializer.SerializeToNode(allowedValue, serializerOptions.GetTypeInfo(allowedValue.GetType())) is { } valueNode)
591+
{
592+
enumArray.Add(valueNode);
593+
}
594+
}
595+
596+
return enumArray;
597+
}
598+
599+
if (ctx.GetCustomAttribute<DataTypeAttribute>() is { } dataTypeAttribute)
600+
{
601+
JsonObject obj = ConvertSchemaToObject(ref schema);
602+
switch (dataTypeAttribute.DataType)
603+
{
604+
case DataType.DateTime:
605+
obj[FormatPropertyName] ??= "date-time";
606+
break;
607+
608+
case DataType.Date:
609+
obj[FormatPropertyName] ??= "date";
610+
break;
611+
612+
case DataType.Time:
613+
obj[FormatPropertyName] ??= "time";
614+
break;
615+
616+
case DataType.Duration:
617+
obj[FormatPropertyName] ??= "duration";
618+
break;
619+
620+
case DataType.EmailAddress:
621+
obj[FormatPropertyName] ??= "email";
622+
break;
623+
624+
case DataType.Url:
625+
obj[FormatPropertyName] ??= "uri";
626+
break;
627+
628+
case DataType.ImageUrl:
629+
obj[FormatPropertyName] ??= "uri";
630+
obj[ContentMediaTypePropertyName] ??= "image/*";
631+
break;
632+
}
633+
}
634+
#endif
635+
}
348636
}
349637
}
350638

0 commit comments

Comments
 (0)