3
3
4
4
using System ;
5
5
using System . ComponentModel ;
6
+ #if NET || NETFRAMEWORK
7
+ using System . ComponentModel . DataAnnotations ;
8
+ #endif
6
9
using System . Diagnostics ;
7
10
using System . Diagnostics . CodeAnalysis ;
8
11
using System . Reflection ;
14
17
using System . Threading ;
15
18
using Microsoft . Shared . Diagnostics ;
16
19
17
- #pragma warning disable S1121 // Assignments should not be made from within sub-expressions
18
20
#pragma warning disable S107 // Methods should not have too many parameters
21
+ #pragma warning disable S109 // Magic numbers should not be used
19
22
#pragma warning disable S1075 // URIs should not be hardcoded
23
+ #pragma warning disable S1121 // Assignments should not be made from within sub-expressions
24
+ #pragma warning disable S1199 // Nested block
20
25
#pragma warning disable SA1118 // Parameter should not span multiple lines
21
- #pragma warning disable S109 // Magic numbers should not be used
22
26
23
27
namespace Microsoft . Extensions . AI ;
24
28
@@ -38,14 +42,25 @@ public static partial class AIJsonUtilities
38
42
private const string AdditionalPropertiesPropertyName = "additionalProperties" ;
39
43
private const string DefaultPropertyName = "default" ;
40
44
private const string RefPropertyName = "$ref" ;
45
+ #if NET || NETFRAMEWORK
46
+ private const string FormatPropertyName = "format" ;
47
+ private const string MinLengthStringPropertyName = "minLength" ;
48
+ private const string MaxLengthStringPropertyName = "maxLength" ;
49
+ private const string MinLengthCollectionPropertyName = "minItems" ;
50
+ private const string MaxLengthCollectionPropertyName = "maxItems" ;
51
+ private const string MinRangePropertyName = "minimum" ;
52
+ private const string MaxRangePropertyName = "maximum" ;
53
+ #endif
54
+ #if NET
55
+ private const string ContentEncodingPropertyName = "contentEncoding" ;
56
+ private const string ContentMediaTypePropertyName = "contentMediaType" ;
57
+ private const string MinExclusiveRangePropertyName = "exclusiveMinimum" ;
58
+ private const string MaxExclusiveRangePropertyName = "exclusiveMaximum" ;
59
+ #endif
41
60
42
61
/// <summary>The uri used when populating the $schema keyword in created schemas.</summary>
43
62
private const string SchemaKeywordUri = "https://json-schema.org/draft/2020-12/schema" ;
44
63
45
- // List of keywords used by JsonSchemaExporter but explicitly disallowed by some AI vendors.
46
- // cf. https://platform.openai.com/docs/guides/structured-outputs#some-type-specific-keywords-are-not-yet-supported
47
- private static readonly string [ ] _schemaKeywordsDisallowedByAIVendors = [ "minLength" , "maxLength" , "pattern" , "format" ] ;
48
-
49
64
/// <summary>
50
65
/// Determines a JSON schema for the provided method.
51
66
/// </summary>
@@ -280,12 +295,6 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js
280
295
objSchema . InsertAtStart ( TypePropertyName , new JsonArray { ( JsonNode ) "string" , ( JsonNode ) "null" } ) ;
281
296
}
282
297
283
- // Filter potentially disallowed keywords.
284
- foreach ( string keyword in _schemaKeywordsDisallowedByAIVendors )
285
- {
286
- _ = objSchema . Remove ( keyword ) ;
287
- }
288
-
289
298
// Some consumers of the JSON schema, including Ollama as of v0.3.13, don't understand
290
299
// schemas with "type": [...], and only understand "type" being a single value.
291
300
// In certain configurations STJ represents .NET numeric types as ["string", "number"], which will then lead to an error.
@@ -318,6 +327,8 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js
318
327
ConvertSchemaToObject ( ref schema ) . InsertAtStart ( SchemaPropertyName , ( JsonNode ) SchemaKeywordUri ) ;
319
328
}
320
329
330
+ ApplyDataAnnotations ( parameterName , ref schema , ctx ) ;
331
+
321
332
// Finally, apply any user-defined transformations if specified.
322
333
if ( inferenceOptions . TransformSchemaNode is { } transformer )
323
334
{
@@ -345,6 +356,248 @@ static JsonObject ConvertSchemaToObject(ref JsonNode schema)
345
356
return obj ;
346
357
}
347
358
}
359
+
360
+ void ApplyDataAnnotations ( string ? parameterName , ref JsonNode schema , AIJsonSchemaCreateContext ctx )
361
+ {
362
+ if ( ctx . GetCustomAttribute < DisplayNameAttribute > ( ) is { } displayNameAttribute )
363
+ {
364
+ ConvertSchemaToObject ( ref schema ) [ TitlePropertyName ] ??= displayNameAttribute . DisplayName ;
365
+ }
366
+
367
+ #if NET || NETFRAMEWORK
368
+ if ( ctx . GetCustomAttribute < EmailAddressAttribute > ( ) is { } emailAttribute )
369
+ {
370
+ ConvertSchemaToObject ( ref schema ) [ FormatPropertyName ] ??= "email" ;
371
+ }
372
+
373
+ if ( ctx . GetCustomAttribute < UrlAttribute > ( ) is { } urlAttribute )
374
+ {
375
+ ConvertSchemaToObject ( ref schema ) [ FormatPropertyName ] ??= "uri" ;
376
+ }
377
+
378
+ if ( ctx . GetCustomAttribute < RegularExpressionAttribute > ( ) is { } regexAttribute )
379
+ {
380
+ ConvertSchemaToObject ( ref schema ) [ PatternPropertyName ] ??= regexAttribute . Pattern ;
381
+ }
382
+
383
+ if ( ctx . GetCustomAttribute < StringLengthAttribute > ( ) is { } stringLengthAttribute )
384
+ {
385
+ JsonObject obj = ConvertSchemaToObject ( ref schema ) ;
386
+
387
+ if ( stringLengthAttribute . MinimumLength > 0 )
388
+ {
389
+ obj [ MinLengthStringPropertyName ] ??= stringLengthAttribute . MinimumLength ;
390
+ }
391
+
392
+ obj [ MaxLengthStringPropertyName ] ??= stringLengthAttribute . MaximumLength ;
393
+ }
394
+
395
+ if ( ctx . GetCustomAttribute < MinLengthAttribute > ( ) is { } minLengthAttribute )
396
+ {
397
+ JsonObject obj = ConvertSchemaToObject ( ref schema ) ;
398
+ if ( obj [ TypePropertyName ] is JsonNode typeNode && typeNode . GetValueKind ( ) is JsonValueKind . String && typeNode . GetValue < string > ( ) is "string" )
399
+ {
400
+ obj [ MinLengthStringPropertyName ] ??= minLengthAttribute . Length ;
401
+ }
402
+ else
403
+ {
404
+ obj [ MinLengthCollectionPropertyName ] ??= minLengthAttribute . Length ;
405
+ }
406
+ }
407
+
408
+ if ( ctx . GetCustomAttribute < MaxLengthAttribute > ( ) is { } maxLengthAttribute )
409
+ {
410
+ JsonObject obj = ConvertSchemaToObject ( ref schema ) ;
411
+ if ( obj [ TypePropertyName ] is JsonNode typeNode && typeNode . GetValueKind ( ) is JsonValueKind . String && typeNode . GetValue < string > ( ) is "string" )
412
+ {
413
+ obj [ MaxLengthStringPropertyName ] ??= maxLengthAttribute . Length ;
414
+ }
415
+ else
416
+ {
417
+ obj [ MaxLengthCollectionPropertyName ] ??= maxLengthAttribute . Length ;
418
+ }
419
+ }
420
+
421
+ if ( ctx . GetCustomAttribute < RangeAttribute > ( ) is { } rangeAttribute )
422
+ {
423
+ JsonObject obj = ConvertSchemaToObject ( ref schema ) ;
424
+
425
+ JsonNode ? minNode = null ;
426
+ JsonNode ? maxNode = null ;
427
+ switch ( rangeAttribute . Minimum )
428
+ {
429
+ case int minInt32 when rangeAttribute . Maximum is int maxInt32 :
430
+ maxNode = maxInt32 ;
431
+ if (
432
+ #if NET
433
+ ! rangeAttribute . MinimumIsExclusive ||
434
+ #endif
435
+ minInt32 > 0 )
436
+ {
437
+ minNode = minInt32 ;
438
+ }
439
+
440
+ break;
441
+
442
+ case double minDouble when rangeAttribute. Maximum is double maxDouble :
443
+ maxNode = maxDouble;
444
+ if (
445
+ #if NET
446
+ ! rangeAttribute. MinimumIsExclusive ||
447
+ #endif
448
+ minDouble > 0 )
449
+ {
450
+ minNode = minDouble ;
451
+ }
452
+
453
+ break;
454
+
455
+ case string minString when rangeAttribute. Maximum is string maxString :
456
+ maxNode = maxString;
457
+ minNode = minString;
458
+ break ;
459
+ }
460
+
461
+ if ( minNode is not null )
462
+ {
463
+ #if NET
464
+ if ( rangeAttribute . MinimumIsExclusive )
465
+ {
466
+ obj [ MinExclusiveRangePropertyName ] ??= minNode ;
467
+ }
468
+ else
469
+ #endif
470
+ {
471
+ obj[ MinRangePropertyName ] ??= minNode ;
472
+ }
473
+ }
474
+
475
+ if ( maxNode is not null )
476
+ {
477
+ #if NET
478
+ if ( rangeAttribute . MaximumIsExclusive )
479
+ {
480
+ obj[ MaxExclusiveRangePropertyName ] ??= maxNode ;
481
+ }
482
+ else
483
+ #endif
484
+ {
485
+ obj[ MaxRangePropertyName ] ??= maxNode ;
486
+ }
487
+ }
488
+ }
489
+ #endif
490
+
491
+ #if NET
492
+ if ( ctx . GetCustomAttribute < Base64StringAttribute > ( ) is { } base64Attribute )
493
+ {
494
+ ConvertSchemaToObject( ref schema ) [ ContentEncodingPropertyName ] ??= "base64" ;
495
+ }
496
+
497
+ if ( ctx . GetCustomAttribute < LengthAttribute > ( ) is { } lengthAttribute )
498
+ {
499
+ JsonObject obj = ConvertSchemaToObject( ref schema ) ;
500
+
501
+ if ( obj [ TypePropertyName ] is JsonNode typeNode && typeNode . GetValueKind ( ) is JsonValueKind . String && typeNode . GetValue < string > ( ) is "string" )
502
+ {
503
+ if ( lengthAttribute . MinimumLength > 0 )
504
+ {
505
+ obj[ MinLengthStringPropertyName ] ??= lengthAttribute . MinimumLength ;
506
+ }
507
+
508
+ obj[ MaxLengthStringPropertyName ] ??= lengthAttribute . MaximumLength ;
509
+ }
510
+ else
511
+ {
512
+ if ( lengthAttribute . MinimumLength > 0 )
513
+ {
514
+ obj [ MinLengthCollectionPropertyName ] ??= lengthAttribute . MinimumLength ;
515
+ }
516
+
517
+ obj [ MaxLengthCollectionPropertyName ] ??= lengthAttribute . MaximumLength ;
518
+ }
519
+ }
520
+
521
+ if ( ctx . GetCustomAttribute < AllowedValuesAttribute > ( ) is { } allowedValuesAttribute )
522
+ {
523
+ JsonObject obj = ConvertSchemaToObject( ref schema ) ;
524
+ if ( ! obj . ContainsKey ( EnumPropertyName ) )
525
+ {
526
+ if ( CreateJsonArray ( allowedValuesAttribute . Values , serializerOptions ) is { Count : > 0 } enumArray )
527
+ {
528
+ obj [ EnumPropertyName ] = enumArray ;
529
+ }
530
+ }
531
+ }
532
+
533
+ if ( ctx . GetCustomAttribute < DeniedValuesAttribute > ( ) is { } deniedValuesAttribute )
534
+ {
535
+ JsonObject obj = ConvertSchemaToObject ( ref schema ) ;
536
+
537
+ JsonNode ? notNode = obj [ NotPropertyName ] ;
538
+ if ( notNode is null or JsonObject )
539
+ {
540
+ JsonObject notObj =
541
+ notNode as JsonObject ??
542
+ ( JsonObject ) ( obj [ NotPropertyName ] = new JsonObject ( ) ) ;
543
+
544
+ if ( notObj [ EnumPropertyName ] is null )
545
+ {
546
+ if ( CreateJsonArray ( deniedValuesAttribute . Values , serializerOptions ) is { Count : > 0 } enumArray )
547
+ {
548
+ notObj[ EnumPropertyName ] = enumArray ;
549
+ }
550
+ }
551
+ }
552
+ }
553
+
554
+ static JsonArray CreateJsonArray ( object ? [ ] values , JsonSerializerOptions serializerOptions )
555
+ {
556
+ JsonArray enumArray = new ( ) ;
557
+ foreach ( object ? allowedValue in values)
558
+ {
559
+ if ( allowedValue is not null && JsonSerializer . SerializeToNode ( allowedValue , serializerOptions . GetTypeInfo ( allowedValue . GetType ( ) ) ) is { } valueNode )
560
+ {
561
+ enumArray . Add ( valueNode ) ;
562
+ }
563
+ }
564
+
565
+ return enumArray;
566
+ }
567
+
568
+ if ( ctx . GetCustomAttribute < DataTypeAttribute > ( ) is { } dataTypeAttribute )
569
+ {
570
+ JsonObject obj = ConvertSchemaToObject( ref schema ) ;
571
+ switch ( dataTypeAttribute . DataType )
572
+ {
573
+ case DataType. DateTime :
574
+ obj[ FormatPropertyName ] ??= "date-time" ;
575
+ break ;
576
+
577
+ case DataType. Date :
578
+ obj[ FormatPropertyName ] ??= "date" ;
579
+ break ;
580
+
581
+ case DataType. Time :
582
+ obj[ FormatPropertyName ] ??= "time" ;
583
+ break ;
584
+
585
+ case DataType. EmailAddress :
586
+ obj[ FormatPropertyName ] ??= "email" ;
587
+ break ;
588
+
589
+ case DataType. Url :
590
+ obj[ FormatPropertyName ] ??= "uri" ;
591
+ break ;
592
+
593
+ case DataType. ImageUrl :
594
+ obj[ FormatPropertyName ] ??= "uri" ;
595
+ obj[ ContentMediaTypePropertyName ] ??= "image/*" ;
596
+ break ;
597
+ }
598
+ }
599
+ #endif
600
+ }
348
601
}
349
602
}
350
603
0 commit comments