55using System . Collections . Concurrent ;
66using System . ComponentModel ;
77using System . Diagnostics ;
8+ #if ! NET9_0_OR_GREATER
9+ using System . Diagnostics . CodeAnalysis ;
10+ #endif
811using System . Linq ;
912using System . Reflection ;
1013using System . Runtime . CompilerServices ;
1619#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
1720#pragma warning disable S107 // Methods should not have too many parameters
1821#pragma warning disable S1075 // URIs should not be hardcoded
22+ #pragma warning disable SA1118 // Parameter should not span multiple lines
1923
2024using FunctionParameterKey = (
2125 System . Type ? Type ,
@@ -176,6 +180,11 @@ private static JsonElement GetJsonSchemaCached(JsonSerializerOptions options, Fu
176180#endif
177181 }
178182
183+ #if ! NET9_0_OR_GREATER
184+ [ UnconditionalSuppressMessage ( "Trimming" , "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access" ,
185+ Justification = "Pre STJ-9 schema extraction can fail with a runtime exception if certain reflection metadata have been trimmed. " +
186+ "The exception message will guide users to turn off 'IlcTrimMetadata' which resolves all issues." ) ]
187+ #endif
179188 private static JsonElement GetJsonSchemaCore( JsonSerializerOptions options , FunctionParameterKey key )
180189 {
181190 _ = Throw . IfNull ( options ) ;
@@ -238,16 +247,9 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema)
238247 const string DefaultPropertyName = "default" ;
239248 const string RefPropertyName = "$ref" ;
240249
241- // Find the first DescriptionAttribute, starting first from the property, then the parameter, and finally the type itself.
242- Type descAttrType = typeof ( DescriptionAttribute ) ;
243- var descriptionAttribute =
244- GetAttrs ( descAttrType , ctx . PropertyInfo ? . AttributeProvider ) ? . FirstOrDefault ( ) ??
245- GetAttrs ( descAttrType , ctx . PropertyInfo ? . AssociatedParameter ? . AttributeProvider ) ? . FirstOrDefault ( ) ??
246- GetAttrs ( descAttrType , ctx . TypeInfo . Type ) ? . FirstOrDefault ( ) ;
247-
248- if ( descriptionAttribute is DescriptionAttribute attr )
250+ if ( ctx . ResolveAttribute < DescriptionAttribute > ( ) is { } attr )
249251 {
250- ConvertSchemaToObject ( ref schema ) . Insert ( 0 , DescriptionPropertyName , ( JsonNode ) attr . Description ) ;
252+ ConvertSchemaToObject ( ref schema ) . InsertAtStart ( DescriptionPropertyName , ( JsonNode ) attr . Description ) ;
251253 }
252254
253255 if ( schema is JsonObject objSchema )
@@ -270,7 +272,7 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema)
270272 // Include the type keyword in enum types
271273 if ( key . IncludeTypeInEnumSchemas && ctx . TypeInfo . Type . IsEnum && objSchema . ContainsKey ( EnumPropertyName ) && ! objSchema . ContainsKey ( TypePropertyName ) )
272274 {
273- objSchema . Insert ( 0 , TypePropertyName , "string" ) ;
275+ objSchema. InsertAtStart ( TypePropertyName , "string" ) ;
274276 }
275277
276278 // Disallow additional properties in object schemas
@@ -305,7 +307,7 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema)
305307 if ( index < 0 )
306308 {
307309 // If there's no description property, insert it at the beginning of the doc.
308- obj . Insert ( 0 , DescriptionPropertyName , ( JsonNode ) key . Description ! ) ;
310+ obj. InsertAtStart ( DescriptionPropertyName , ( JsonNode ) key . Description ! ) ;
309311 }
310312 else
311313 {
@@ -323,15 +325,12 @@ JsonNode TransformSchemaNode(JsonSchemaExporterContext ctx, JsonNode schema)
323325 if ( key . IncludeSchemaUri )
324326 {
325327 // The $schema property must be the first keyword in the object
326- ConvertSchemaToObject ( ref schema ) . Insert ( 0 , SchemaPropertyName , ( JsonNode ) SchemaKeywordUri ) ;
328+ ConvertSchemaToObject ( ref schema ) . InsertAtStart ( SchemaPropertyName , ( JsonNode ) SchemaKeywordUri ) ;
327329 }
328330 }
329331
330332 return schema ;
331333
332- static object [ ] ? GetAttrs ( Type attrType , ICustomAttributeProvider ? provider ) =>
333- provider ? . GetCustomAttributes ( attrType , inherit : false ) ;
334-
335334 static JsonObject ConvertSchemaToObject ( ref JsonNode schema )
336335 {
337336 JsonObject obj ;
@@ -370,6 +369,62 @@ private static bool TypeIsArrayContainingInteger(JsonNode schema)
370369 return false;
371370 }
372371
372+ private static void InsertAtStart ( this JsonObject jsonObject , string key , JsonNode value )
373+ {
374+ #if NET9_0_OR_GREATER
375+ jsonObject. Insert( 0 , key, value) ;
376+ #else
377+ jsonObject. Remove( key ) ;
378+ var copiedEntries = jsonObject . ToArray ( ) ;
379+ jsonObject . Clear ( ) ;
380+
381+ jsonObject . Add ( key , value ) ;
382+ foreach ( var entry in copiedEntries )
383+ {
384+ jsonObject [ entry . Key ] = entry . Value ;
385+ }
386+ #endif
387+ }
388+
389+ #if ! NET9_0_OR_GREATER
390+ private static int IndexOf ( this JsonObject jsonObject , string key )
391+ {
392+ int i = 0 ;
393+ foreach ( var entry in jsonObject )
394+ {
395+ if ( string . Equals ( entry . Key , key , StringComparison . Ordinal ) )
396+ {
397+ return i ;
398+ }
399+
400+ i ++ ;
401+ }
402+
403+ return - 1 ;
404+ }
405+ #endif
406+
407+ private static TAttribute ? ResolveAttribute < TAttribute > ( this JsonSchemaExporterContext ctx)
408+ where TAttribute : Attribute
409+ {
410+ // Resolve attributes from locations in the following order:
411+ // 1. Property-level attributes
412+ // 2. Parameter-level attributes and
413+ // 3. Type-level attributes.
414+ return
415+ #if NET9_0_OR_GREATER
416+ GetAttrs( ctx. PropertyInfo? . AttributeProvider) ??
417+ GetAttrs ( ctx . PropertyInfo ? . AssociatedParameter ? . AttributeProvider ) ??
418+ #else
419+ GetAttrs( ctx . PropertyAttributeProvider ) ??
420+ GetAttrs( ctx . ParameterInfo ) ??
421+ #endif
422+ GetAttrs( ctx . TypeInfo . Type ) ;
423+
424+ static TAttribute ? GetAttrs ( ICustomAttributeProvider ? provider ) =>
425+ ( TAttribute ? ) provider? . GetCustomAttributes( typeof ( TAttribute ) , inherit : false) . FirstOrDefault ( ) ;
426+ }
427+
373428 private static JsonElement ParseJsonElement( ReadOnlySpan < byte > utf8Json )
374429 {
375430 Utf8JsonReader reader = new ( utf8Json) ;
0 commit comments