33
44using System ;
55using System . ComponentModel ;
6- #if NET
6+ #if NET || NETFRAMEWORK
77using System . ComponentModel . DataAnnotations ;
88#endif
99using System . Diagnostics ;
2121#pragma warning disable S109 // Magic numbers should not be used
2222#pragma warning disable S1075 // URIs should not be hardcoded
2323#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
24+ #pragma warning disable S1199 // Nested block
2425#pragma warning disable SA1118 // Parameter should not span multiple lines
2526
2627namespace Microsoft . Extensions . AI ;
@@ -41,27 +42,25 @@ public static partial class AIJsonUtilities
4142 private const string AdditionalPropertiesPropertyName = "additionalProperties" ;
4243 private const string DefaultPropertyName = "default" ;
4344 private const string RefPropertyName = "$ref" ;
45+ #if NET || NETFRAMEWORK
4446 private const string FormatPropertyName = "format" ;
45- #if NET
46- private const string ContentEncodingPropertyName = "contentEncoding" ;
47- private const string ContentMediaTypePropertyName = "contentMediaType" ;
4847 private const string MinLengthStringPropertyName = "minLength" ;
4948 private const string MaxLengthStringPropertyName = "maxLength" ;
5049 private const string MinLengthCollectionPropertyName = "minItems" ;
5150 private const string MaxLengthCollectionPropertyName = "maxItems" ;
5251 private const string MinRangePropertyName = "minimum" ;
5352 private const string MaxRangePropertyName = "maximum" ;
53+ #endif
54+ #if NET
55+ private const string ContentEncodingPropertyName = "contentEncoding" ;
56+ private const string ContentMediaTypePropertyName = "contentMediaType" ;
5457 private const string MinExclusiveRangePropertyName = "exclusiveMinimum" ;
5558 private const string MaxExclusiveRangePropertyName = "exclusiveMaximum" ;
5659#endif
5760
5861 /// <summary>The uri used when populating the $schema keyword in created schemas.</summary>
5962 private const string SchemaKeywordUri = "https://json-schema.org/draft/2020-12/schema" ;
6063
61- // List of keywords used by JsonSchemaExporter but explicitly disallowed by some AI vendors.
62- // cf. https://platform.openai.com/docs/guides/structured-outputs#some-type-specific-keywords-are-not-yet-supported
63- private static readonly string [ ] _schemaKeywordsDisallowedByAIVendors = [ "minLength" , "maxLength" , "pattern" , "format" ] ;
64-
6564 /// <summary>
6665 /// Determines a JSON schema for the provided method.
6766 /// </summary>
@@ -296,12 +295,6 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js
296295 objSchema . InsertAtStart ( TypePropertyName , new JsonArray { ( JsonNode ) "string" , ( JsonNode ) "null" } ) ;
297296 }
298297
299- // Filter potentially disallowed keywords.
300- foreach ( string keyword in _schemaKeywordsDisallowedByAIVendors )
301- {
302- _ = objSchema . Remove ( keyword ) ;
303- }
304-
305298 // Some consumers of the JSON schema, including Ollama as of v0.3.13, don't understand
306299 // schemas with "type": [...], and only understand "type" being a single value.
307300 // In certain configurations STJ represents .NET numeric types as ["string", "number"], which will then lead to an error.
@@ -334,7 +327,6 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext schemaExporterContext, Js
334327 ConvertSchemaToObject ( ref schema ) . InsertAtStart ( SchemaPropertyName , ( JsonNode ) SchemaKeywordUri ) ;
335328 }
336329
337- ApplyDataTypeFormats ( parameterName , ref schema , ctx ) ;
338330 ApplyDataAnnotations ( parameterName , ref schema , ctx ) ;
339331
340332 // Finally, apply any user-defined transformations if specified.
@@ -365,56 +357,14 @@ static JsonObject ConvertSchemaToObject(ref JsonNode schema)
365357 }
366358 }
367359
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-
405360 void ApplyDataAnnotations ( string ? parameterName , ref JsonNode schema , AIJsonSchemaCreateContext ctx )
406361 {
407362 if ( ctx . GetCustomAttribute < DisplayNameAttribute > ( ) is { } displayNameAttribute )
408363 {
409364 ConvertSchemaToObject ( ref schema ) [ TitlePropertyName ] ??= displayNameAttribute . DisplayName ;
410365 }
411366
412- #if NET
413- if ( ctx . GetCustomAttribute < Base64StringAttribute > ( ) is { } base64Attribute )
414- {
415- ConvertSchemaToObject ( ref schema ) [ ContentEncodingPropertyName ] ??= "base64" ;
416- }
417-
367+ #if NET || NETFRAMEWORK
418368 if ( ctx . GetCustomAttribute < EmailAddressAttribute > ( ) is { } emailAttribute )
419369 {
420370 ConvertSchemaToObject ( ref schema ) [ FormatPropertyName ] ??= "email" ;
@@ -442,30 +392,6 @@ void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSche
442392 obj [ MaxLengthStringPropertyName ] ??= stringLengthAttribute . MaximumLength ;
443393 }
444394
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-
469395 if ( ctx . GetCustomAttribute < MinLengthAttribute > ( ) is { } minLengthAttribute )
470396 {
471397 JsonObject obj = ConvertSchemaToObject ( ref schema ) ;
@@ -502,7 +428,11 @@ void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSche
502428 {
503429 case int minInt32 when rangeAttribute . Maximum is int maxInt32 :
504430 maxNode = maxInt32 ;
505- if ( ! rangeAttribute . MinimumIsExclusive || minInt32 > 0 )
431+ if (
432+ #if NET
433+ ! rangeAttribute . MinimumIsExclusive ||
434+ #endif
435+ minInt32 > 0 )
506436 {
507437 minNode = minInt32 ;
508438 }
@@ -511,7 +441,11 @@ void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSche
511441
512442 case double minDouble when rangeAttribute. Maximum is double maxDouble :
513443 maxNode = maxDouble;
514- if ( ! rangeAttribute . MinimumIsExclusive || minDouble > 0 )
444+ if (
445+ #if NET
446+ ! rangeAttribute. MinimumIsExclusive ||
447+ #endif
448+ minDouble > 0 )
515449 {
516450 minNode = minDouble ;
517451 }
@@ -526,28 +460,63 @@ void ApplyDataAnnotations(string? parameterName, ref JsonNode schema, AIJsonSche
526460
527461 if ( minNode is not null )
528462 {
463+ #if NET
529464 if ( rangeAttribute . MinimumIsExclusive )
530465 {
531466 obj [ MinExclusiveRangePropertyName ] ??= minNode ;
532467 }
533468 else
469+ #endif
534470 {
535471 obj[ MinRangePropertyName ] ??= minNode ;
536472 }
537473 }
538474
539475 if ( maxNode is not null )
540476 {
477+ #if NET
541478 if ( rangeAttribute . MaximumIsExclusive )
542479 {
543480 obj[ MaxExclusiveRangePropertyName ] ??= maxNode ;
544481 }
545482 else
483+ #endif
546484 {
547485 obj[ MaxRangePropertyName ] ??= maxNode ;
548486 }
549487 }
550488 }
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+ }
551520
552521 if ( ctx . GetCustomAttribute < AllowedValuesAttribute > ( ) is { } allowedValuesAttribute )
553522 {
@@ -613,10 +582,6 @@ static JsonArray CreateJsonArray(object?[] values, JsonSerializerOptions seriali
613582 obj[ FormatPropertyName ] ??= "time" ;
614583 break ;
615584
616- case DataType . Duration :
617- obj [ FormatPropertyName ] ??= "duration" ;
618- break ;
619-
620585 case DataType. EmailAddress :
621586 obj[ FormatPropertyName ] ??= "email" ;
622587 break ;
0 commit comments