88using System . Reflection ;
99using System . Text . Json ;
1010using System . Text . Json . Nodes ;
11+ using System . Text . Json . Schema ;
1112using System . Text . Json . Serialization . Metadata ;
12- using JsonSchemaMapper ;
1313using Microsoft . AspNetCore . Mvc . ApiExplorer ;
1414using Microsoft . AspNetCore . Mvc . Infrastructure ;
1515using Microsoft . AspNetCore . Routing ;
@@ -22,8 +22,10 @@ namespace Microsoft.AspNetCore.OpenApi;
2222/// Provides a set of extension methods for modifying the opaque JSON Schema type
2323/// that is provided by the underlying schema generator in System.Text.Json.
2424/// </summary>
25- internal static class JsonObjectSchemaExtensions
25+ internal static class JsonNodeSchemaExtensions
2626{
27+ private static readonly NullabilityInfoContext _nullabilityInfoContext = new ( ) ;
28+
2729 private static readonly Dictionary < Type , OpenApiSchema > _simpleTypeToOpenApiSchema = new ( )
2830 {
2931 [ typeof ( bool ) ] = new ( ) { Type = "boolean" } ,
@@ -43,6 +45,8 @@ internal static class JsonObjectSchemaExtensions
4345 [ typeof ( char ) ] = new ( ) { Type = "string" } ,
4446 [ typeof ( Uri ) ] = new ( ) { Type = "string" , Format = "uri" } ,
4547 [ typeof ( string ) ] = new ( ) { Type = "string" } ,
48+ [ typeof ( TimeOnly ) ] = new ( ) { Type = "string" , Format = "time" } ,
49+ [ typeof ( DateOnly ) ] = new ( ) { Type = "string" , Format = "date" } ,
4650 } ;
4751
4852 /// <summary>
@@ -52,7 +56,7 @@ internal static class JsonObjectSchemaExtensions
5256 /// OpenApi schema v3 supports the validation vocabulary supported by JSON Schema. Because the underlying
5357 /// schema generator does not handle validation attributes to the validation vocabulary, we apply that mapping here.
5458 ///
55- /// Note that this method targets <see cref="JsonObject "/> and not <see cref="OpenApiSchema"/> because it is
59+ /// Note that this method targets <see cref="JsonNode "/> and not <see cref="OpenApiSchema"/> because it is
5660 /// designed to be invoked via the `OnGenerated` callback provided by the underlying schema generator
5761 /// so that attributes can be mapped to the properties associated with inputs and outputs to a given request.
5862 ///
@@ -74,9 +78,9 @@ internal static class JsonObjectSchemaExtensions
7478 /// will result in the schema having a type of "string" and a format of "uri" even though the model binding
7579 /// layer will validate the string against *both* constraints.
7680 /// </remarks>
77- /// <param name="schema">The <see cref="JsonObject "/> produced by the underlying schema generator.</param>
81+ /// <param name="schema">The <see cref="JsonNode "/> produced by the underlying schema generator.</param>
7882 /// <param name="validationAttributes">A list of the validation attributes to apply.</param>
79- internal static void ApplyValidationAttributes ( this JsonObject schema , IEnumerable < Attribute > validationAttributes )
83+ internal static void ApplyValidationAttributes ( this JsonNode schema , IEnumerable < Attribute > validationAttributes )
8084 {
8185 foreach ( var attribute in validationAttributes )
8286 {
@@ -126,10 +130,10 @@ internal static void ApplyValidationAttributes(this JsonObject schema, IEnumerab
126130 /// <summary>
127131 /// Populate the default value into the current schema.
128132 /// </summary>
129- /// <param name="schema">The <see cref="JsonObject "/> produced by the underlying schema generator.</param>
133+ /// <param name="schema">The <see cref="JsonNode "/> produced by the underlying schema generator.</param>
130134 /// <param name="defaultValue">An object representing the <see cref="object"/> associated with the default value.</param>
131135 /// <param name="jsonTypeInfo">The <see cref="JsonTypeInfo"/> associated with the target type.</param>
132- internal static void ApplyDefaultValue ( this JsonObject schema , object ? defaultValue , JsonTypeInfo ? jsonTypeInfo )
136+ internal static void ApplyDefaultValue ( this JsonNode schema , object ? defaultValue , JsonTypeInfo ? jsonTypeInfo )
133137 {
134138 if ( jsonTypeInfo is null )
135139 {
@@ -159,29 +163,35 @@ internal static void ApplyDefaultValue(this JsonObject schema, object? defaultVa
159163 /// based on whether the underlying schema generator returned an array type containing "null" to
160164 /// represent a nullable type or if the type was denoted as nullable from our lookup cache.
161165 ///
162- /// Note that this method targets <see cref="JsonObject "/> and not <see cref="OpenApiSchema"/> because
166+ /// Note that this method targets <see cref="JsonNode "/> and not <see cref="OpenApiSchema"/> because
163167 /// it is is designed to be invoked via the `OnGenerated` callback in the underlying schema generator as
164168 /// opposed to after the generated schemas have been mapped to OpenAPI schemas.
165169 /// </remarks>
166- /// <param name="schema">The <see cref="JsonObject "/> produced by the underlying schema generator.</param>
167- /// <param name="context">The <see cref="JsonSchemaGenerationContext "/> associated with the <see paramref="schema"/>.</param>
168- internal static void ApplyPrimitiveTypesAndFormats ( this JsonObject schema , JsonSchemaGenerationContext context )
170+ /// <param name="schema">The <see cref="JsonNode "/> produced by the underlying schema generator.</param>
171+ /// <param name="context">The <see cref="JsonSchemaExporterContext "/> associated with the <see paramref="schema"/>.</param>
172+ internal static void ApplyPrimitiveTypesAndFormats ( this JsonNode schema , JsonSchemaExporterContext context )
169173 {
170- if ( _simpleTypeToOpenApiSchema . TryGetValue ( context . TypeInfo . Type , out var openApiSchema ) )
174+ var type = context . TypeInfo . Type ;
175+ var underlyingType = Nullable . GetUnderlyingType ( type ) ;
176+ if ( _simpleTypeToOpenApiSchema . TryGetValue ( underlyingType ?? type , out var openApiSchema ) )
171177 {
172178 schema [ OpenApiSchemaKeywords . NullableKeyword ] = openApiSchema . Nullable || ( schema [ OpenApiSchemaKeywords . TypeKeyword ] is JsonArray schemaType && schemaType . GetValues < string > ( ) . Contains ( "null" ) ) ;
173179 schema [ OpenApiSchemaKeywords . TypeKeyword ] = openApiSchema . Type ;
174180 schema [ OpenApiSchemaKeywords . FormatKeyword ] = openApiSchema . Format ;
175181 schema [ OpenApiConstants . SchemaId ] = context . TypeInfo . GetSchemaReferenceId ( ) ;
182+ schema [ OpenApiSchemaKeywords . NullableKeyword ] = underlyingType != null ;
183+ // Clear out patterns that the underlying JSON schema generator uses to represent
184+ // validations for DateTime, DateTimeOffset, and integers.
185+ schema [ OpenApiSchemaKeywords . PatternKeyword ] = null ;
176186 }
177187 }
178188
179189 /// <summary>
180190 /// Applies route constraints to the target schema.
181191 /// </summary>
182- /// <param name="schema">The <see cref="JsonObject "/> produced by the underlying schema generator.</param>
192+ /// <param name="schema">The <see cref="JsonNode "/> produced by the underlying schema generator.</param>
183193 /// <param name="constraints">The list of <see cref="IRouteConstraint"/>s associated with the route parameter.</param>
184- internal static void ApplyRouteConstraints ( this JsonObject schema , IEnumerable < IRouteConstraint > constraints )
194+ internal static void ApplyRouteConstraints ( this JsonNode schema , IEnumerable < IRouteConstraint > constraints )
185195 {
186196 // Apply constraints in reverse order because when it comes to the routing
187197 // layer the first constraint that is violated causes routing to short circuit.
@@ -255,10 +265,10 @@ internal static void ApplyRouteConstraints(this JsonObject schema, IEnumerable<I
255265 /// <summary>
256266 /// Applies parameter-specific customizations to the target schema.
257267 /// </summary>
258- /// <param name="schema">The <see cref="JsonObject "/> produced by the underlying schema generator.</param>
268+ /// <param name="schema">The <see cref="JsonNode "/> produced by the underlying schema generator.</param>
259269 /// <param name="parameterDescription">The <see cref="ApiParameterDescription"/> associated with the <see paramref="schema"/>.</param>
260270 /// <param name="jsonTypeInfo">The <see cref="JsonTypeInfo"/> associated with the <see paramref="schema"/>.</param>
261- internal static void ApplyParameterInfo ( this JsonObject schema , ApiParameterDescription parameterDescription , JsonTypeInfo ? jsonTypeInfo )
271+ internal static void ApplyParameterInfo ( this JsonNode schema , ApiParameterDescription parameterDescription , JsonTypeInfo ? jsonTypeInfo )
262272 {
263273 // This is special handling for parameters that are not bound from the body but represented in a complex type.
264274 // For example:
@@ -281,17 +291,24 @@ internal static void ApplyParameterInfo(this JsonObject schema, ApiParameterDesc
281291 {
282292 var attributes = validations . OfType < ValidationAttribute > ( ) ;
283293 schema . ApplyValidationAttributes ( attributes ) ;
284- if ( parameterDescription . ParameterDescriptor is IParameterInfoParameterDescriptor { ParameterInfo : { } parameterInfo } )
294+ }
295+ if ( parameterDescription . ParameterDescriptor is IParameterInfoParameterDescriptor { ParameterInfo : { } parameterInfo } )
296+ {
297+ if ( parameterInfo . HasDefaultValue )
285298 {
286- if ( parameterInfo . HasDefaultValue )
287- {
288- schema . ApplyDefaultValue ( parameterInfo . DefaultValue , jsonTypeInfo ) ;
289- }
290- else if ( parameterInfo . GetCustomAttributes < DefaultValueAttribute > ( ) . LastOrDefault ( ) is { } defaultValueAttribute )
291- {
292- schema . ApplyDefaultValue ( defaultValueAttribute . Value , jsonTypeInfo ) ;
293- }
299+ schema . ApplyDefaultValue ( parameterInfo . DefaultValue , jsonTypeInfo ) ;
300+ }
301+ else if ( parameterInfo . GetCustomAttributes < DefaultValueAttribute > ( ) . LastOrDefault ( ) is { } defaultValueAttribute )
302+ {
303+ schema . ApplyDefaultValue ( defaultValueAttribute . Value , jsonTypeInfo ) ;
304+ }
305+
306+ if ( parameterInfo . GetCustomAttributes ( ) . OfType < ValidationAttribute > ( ) is { } validationAttributes )
307+ {
308+ schema . ApplyValidationAttributes ( validationAttributes ) ;
294309 }
310+
311+ schema . ApplyNullabilityContextInfo ( parameterInfo ) ;
295312 }
296313 // Route constraints are only defined on parameters that are sourced from the path. Since
297314 // they are encoded in the route template, and not in the type information based to the underlying
@@ -305,9 +322,9 @@ internal static void ApplyParameterInfo(this JsonObject schema, ApiParameterDesc
305322 /// <summary>
306323 /// Applies the polymorphism options to the target schema following OpenAPI v3's conventions.
307324 /// </summary>
308- /// <param name="schema">The <see cref="JsonObject "/> produced by the underlying schema generator.</param>
309- /// <param name="context">The <see cref="JsonSchemaGenerationContext "/> associated with the current type.</param>
310- internal static void ApplyPolymorphismOptions ( this JsonObject schema , JsonSchemaGenerationContext context )
325+ /// <param name="schema">The <see cref="JsonNode "/> produced by the underlying schema generator.</param>
326+ /// <param name="context">The <see cref="JsonSchemaExporterContext "/> associated with the current type.</param>
327+ internal static void ApplyPolymorphismOptions ( this JsonNode schema , JsonSchemaExporterContext context )
311328 {
312329 if ( context . TypeInfo . PolymorphismOptions is { } polymorphismOptions )
313330 {
@@ -329,10 +346,48 @@ internal static void ApplyPolymorphismOptions(this JsonObject schema, JsonSchema
329346 /// <summary>
330347 /// Set the x-schema-id property on the schema to the identifier associated with the type.
331348 /// </summary>
332- /// <param name="schema">The <see cref="JsonObject "/> produced by the underlying schema generator.</param>
333- /// <param name="context">The <see cref="JsonSchemaGenerationContext "/> associated with the current type.</param>
334- internal static void ApplySchemaReferenceId ( this JsonObject schema , JsonSchemaGenerationContext context )
349+ /// <param name="schema">The <see cref="JsonNode "/> produced by the underlying schema generator.</param>
350+ /// <param name="context">The <see cref="JsonSchemaExporterContext "/> associated with the current type.</param>
351+ internal static void ApplySchemaReferenceId ( this JsonNode schema , JsonSchemaExporterContext context )
335352 {
336353 schema [ OpenApiConstants . SchemaId ] = context . TypeInfo . GetSchemaReferenceId ( ) ;
337354 }
355+
356+ /// <summary>
357+ /// Support applying nullability status for reference types provided as a parameter.
358+ /// </summary>
359+ /// <param name="schema">The <see cref="JsonNode"/> produced by the underlying schema generator.</param>
360+ /// <param name="parameterInfo">The <see cref="ParameterInfo" /> associated with the schema.</param>
361+ internal static void ApplyNullabilityContextInfo ( this JsonNode schema , ParameterInfo parameterInfo )
362+ {
363+ if ( parameterInfo . ParameterType . IsValueType )
364+ {
365+ return ;
366+ }
367+
368+ var nullabilityInfo = _nullabilityInfoContext . Create ( parameterInfo ) ;
369+ if ( nullabilityInfo . WriteState == NullabilityState . Nullable )
370+ {
371+ schema [ OpenApiSchemaKeywords . NullableKeyword ] = true ;
372+ }
373+ }
374+
375+ /// <summary>
376+ /// Support applying nullability status for reference types provided as a property or field.
377+ /// </summary>
378+ /// <param name="schema">The <see cref="JsonNode"/> produced by the underlying schema generator.</param>
379+ /// <param name="attributeProvider">The <see cref="PropertyInfo" /> or <see cref="FieldInfo"/> associated with the schema.</param>
380+ internal static void ApplyNullabilityContextInfo ( this JsonNode schema , ICustomAttributeProvider attributeProvider )
381+ {
382+ var nullabilityInfo = attributeProvider switch
383+ {
384+ PropertyInfo propertyInfo => ! propertyInfo . PropertyType . IsValueType ? _nullabilityInfoContext . Create ( propertyInfo ) : null ,
385+ FieldInfo fieldInfo => ! fieldInfo . FieldType . IsValueType ? _nullabilityInfoContext . Create ( fieldInfo ) : null ,
386+ _ => null
387+ } ;
388+ if ( nullabilityInfo is { WriteState : NullabilityState . Nullable } or { ReadState : NullabilityState . Nullable } )
389+ {
390+ schema [ OpenApiSchemaKeywords . NullableKeyword ] = true ;
391+ }
392+ }
338393}
0 commit comments