33
44using System ;
55using System . ComponentModel ;
6+ #if NET
7+ using System . ComponentModel . DataAnnotations ;
8+ #endif
69using System . Diagnostics ;
710using System . Diagnostics . CodeAnalysis ;
811using System . Reflection ;
1417using System . Threading ;
1518using 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
2326namespace 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