8
8
using System . Reflection ;
9
9
using System . Text . Json ;
10
10
using System . Text . Json . Nodes ;
11
+ using System . Text . Json . Schema ;
11
12
using System . Text . Json . Serialization . Metadata ;
12
- using JsonSchemaMapper ;
13
13
using Microsoft . AspNetCore . Mvc . ApiExplorer ;
14
14
using Microsoft . AspNetCore . Mvc . Infrastructure ;
15
15
using Microsoft . AspNetCore . Routing ;
@@ -22,8 +22,10 @@ namespace Microsoft.AspNetCore.OpenApi;
22
22
/// Provides a set of extension methods for modifying the opaque JSON Schema type
23
23
/// that is provided by the underlying schema generator in System.Text.Json.
24
24
/// </summary>
25
- internal static class JsonObjectSchemaExtensions
25
+ internal static class JsonNodeSchemaExtensions
26
26
{
27
+ private static readonly NullabilityInfoContext _nullabilityInfoContext = new ( ) ;
28
+
27
29
private static readonly Dictionary < Type , OpenApiSchema > _simpleTypeToOpenApiSchema = new ( )
28
30
{
29
31
[ typeof ( bool ) ] = new ( ) { Type = "boolean" } ,
@@ -43,6 +45,8 @@ internal static class JsonObjectSchemaExtensions
43
45
[ typeof ( char ) ] = new ( ) { Type = "string" } ,
44
46
[ typeof ( Uri ) ] = new ( ) { Type = "string" , Format = "uri" } ,
45
47
[ typeof ( string ) ] = new ( ) { Type = "string" } ,
48
+ [ typeof ( TimeOnly ) ] = new ( ) { Type = "string" , Format = "time" } ,
49
+ [ typeof ( DateOnly ) ] = new ( ) { Type = "string" , Format = "date" } ,
46
50
} ;
47
51
48
52
/// <summary>
@@ -52,7 +56,7 @@ internal static class JsonObjectSchemaExtensions
52
56
/// OpenApi schema v3 supports the validation vocabulary supported by JSON Schema. Because the underlying
53
57
/// schema generator does not handle validation attributes to the validation vocabulary, we apply that mapping here.
54
58
///
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
56
60
/// designed to be invoked via the `OnGenerated` callback provided by the underlying schema generator
57
61
/// so that attributes can be mapped to the properties associated with inputs and outputs to a given request.
58
62
///
@@ -74,9 +78,9 @@ internal static class JsonObjectSchemaExtensions
74
78
/// will result in the schema having a type of "string" and a format of "uri" even though the model binding
75
79
/// layer will validate the string against *both* constraints.
76
80
/// </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>
78
82
/// <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 )
80
84
{
81
85
foreach ( var attribute in validationAttributes )
82
86
{
@@ -126,10 +130,10 @@ internal static void ApplyValidationAttributes(this JsonObject schema, IEnumerab
126
130
/// <summary>
127
131
/// Populate the default value into the current schema.
128
132
/// </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>
130
134
/// <param name="defaultValue">An object representing the <see cref="object"/> associated with the default value.</param>
131
135
/// <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 )
133
137
{
134
138
if ( jsonTypeInfo is null )
135
139
{
@@ -159,29 +163,35 @@ internal static void ApplyDefaultValue(this JsonObject schema, object? defaultVa
159
163
/// based on whether the underlying schema generator returned an array type containing "null" to
160
164
/// represent a nullable type or if the type was denoted as nullable from our lookup cache.
161
165
///
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
163
167
/// it is is designed to be invoked via the `OnGenerated` callback in the underlying schema generator as
164
168
/// opposed to after the generated schemas have been mapped to OpenAPI schemas.
165
169
/// </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 )
169
173
{
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 ) )
171
177
{
172
178
schema [ OpenApiSchemaKeywords . NullableKeyword ] = openApiSchema . Nullable || ( schema [ OpenApiSchemaKeywords . TypeKeyword ] is JsonArray schemaType && schemaType . GetValues < string > ( ) . Contains ( "null" ) ) ;
173
179
schema [ OpenApiSchemaKeywords . TypeKeyword ] = openApiSchema . Type ;
174
180
schema [ OpenApiSchemaKeywords . FormatKeyword ] = openApiSchema . Format ;
175
181
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 ;
176
186
}
177
187
}
178
188
179
189
/// <summary>
180
190
/// Applies route constraints to the target schema.
181
191
/// </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>
183
193
/// <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 )
185
195
{
186
196
// Apply constraints in reverse order because when it comes to the routing
187
197
// 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
255
265
/// <summary>
256
266
/// Applies parameter-specific customizations to the target schema.
257
267
/// </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>
259
269
/// <param name="parameterDescription">The <see cref="ApiParameterDescription"/> associated with the <see paramref="schema"/>.</param>
260
270
/// <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 )
262
272
{
263
273
// This is special handling for parameters that are not bound from the body but represented in a complex type.
264
274
// For example:
@@ -281,17 +291,24 @@ internal static void ApplyParameterInfo(this JsonObject schema, ApiParameterDesc
281
291
{
282
292
var attributes = validations . OfType < ValidationAttribute > ( ) ;
283
293
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 )
285
298
{
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 ) ;
294
309
}
310
+
311
+ schema . ApplyNullabilityContextInfo ( parameterInfo ) ;
295
312
}
296
313
// Route constraints are only defined on parameters that are sourced from the path. Since
297
314
// 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
305
322
/// <summary>
306
323
/// Applies the polymorphism options to the target schema following OpenAPI v3's conventions.
307
324
/// </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 )
311
328
{
312
329
if ( context . TypeInfo . PolymorphismOptions is { } polymorphismOptions )
313
330
{
@@ -329,10 +346,48 @@ internal static void ApplyPolymorphismOptions(this JsonObject schema, JsonSchema
329
346
/// <summary>
330
347
/// Set the x-schema-id property on the schema to the identifier associated with the type.
331
348
/// </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 )
335
352
{
336
353
schema [ OpenApiConstants . SchemaId ] = context . TypeInfo . GetSchemaReferenceId ( ) ;
337
354
}
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
+ }
338
393
}
0 commit comments