dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.19k stars 9.93k forks source link

OpenAPI: Range attribute causes FormatException #57390

Open mus65 opened 3 weeks ago

mus65 commented 3 weeks ago

Is there an existing issue for this?

Describe the bug

Adding the following attribute to a property of type long causes a FormatException when generating the OpenAPI document:

[Range(0, 9223372036854775807)]

Affected code: https://github.com/dotnet/aspnetcore/blob/48a07213d4d8df15c6dffccd161844842c196998/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs#L95

Exception:

System.FormatException: The input string '9.223372036854776E+18' was not in a correct format.
   at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
   at System.Number.ParseDecimal[TChar](ReadOnlySpan`1 value, NumberStyles styles, NumberFormatInfo info)
   at System.Decimal.Parse(String s, IFormatProvider provider)
   at Microsoft.AspNetCore.OpenApi.JsonNodeSchemaExtensions.ApplyValidationAttributes(JsonNode schema, IEnumerable`1 validationAttributes)
   at Microsoft.AspNetCore.OpenApi.OpenApiSchemaService.<>c__DisplayClass0_0.<.ctor>b__2(JsonSchemaExporterContext context, JsonNode schema)
   at System.Text.Json.Schema.JsonSchema.<ToJsonNode>g__CompleteSchema|104_0(JsonNode schema, <>c__DisplayClass104_0&)
   at System.Text.Json.Schema.JsonSchema.ToJsonNode(JsonSchemaExporterOptions options)
   at System.Text.Json.Schema.JsonSchema.ToJsonNode(JsonSchemaExporterOptions options)
   at System.Text.Json.Schema.JsonSchema.ToJsonNode(JsonSchemaExporterOptions options)
   at System.Text.Json.Schema.JsonSchema.ToJsonNode(JsonSchemaExporterOptions options)
   at System.Text.Json.Schema.JsonSchemaExporter.GetJsonSchemaAsNode(JsonTypeInfo typeInfo, JsonSchemaExporterOptions exporterOptions)
   at System.Text.Json.Schema.JsonSchemaExporter.GetJsonSchemaAsNode(JsonSerializerOptions options, Type type, JsonSchemaExporterOptions exporterOptions)
   at Microsoft.AspNetCore.OpenApi.OpenApiSchemaService.CreateSchema(OpenApiSchemaKey key)
   at Microsoft.AspNetCore.OpenApi.OpenApiSchemaStore.GetOrAdd(OpenApiSchemaKey key, Func`2 valueFactory)
   at Microsoft.AspNetCore.OpenApi.OpenApiSchemaService.GetOrCreateSchemaAsync(Type type, ApiParameterDescription parameterDescription, Boolean captureSchemaByRef, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetJsonRequestBody(IList`1 supportedRequestFormats, ApiParameterDescription bodyParameter, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetRequestBodyAsync(ApiDescription description, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOperationAsync(ApiDescription description, HashSet`1 capturedTags, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOperationsAsync(IGrouping`2 descriptions, HashSet`1 capturedTags, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOpenApiPathsAsync(HashSet`1 capturedTags, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOpenApiDocumentAsync(CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Builder.OpenApiEndpointRouteBuilderExtensions.<>c__DisplayClass0_0.<<MapOpenApi>b__0>d.MoveNext()

Expected Behavior

No exception.

Steps To Reproduce

Add [Range(0, 9223372036854775807)] to a long property.

Exceptions (if any)

System.FormatException: The input string '9.223372036854776E+18' was not in a correct format.
   at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
   at System.Number.ParseDecimal[TChar](ReadOnlySpan`1 value, NumberStyles styles, NumberFormatInfo info)
   at System.Decimal.Parse(String s, IFormatProvider provider)
   at Microsoft.AspNetCore.OpenApi.JsonNodeSchemaExtensions.ApplyValidationAttributes(JsonNode schema, IEnumerable`1 validationAttributes)
   at Microsoft.AspNetCore.OpenApi.OpenApiSchemaService.<>c__DisplayClass0_0.<.ctor>b__2(JsonSchemaExporterContext context, JsonNode schema)
   at System.Text.Json.Schema.JsonSchema.<ToJsonNode>g__CompleteSchema|104_0(JsonNode schema, <>c__DisplayClass104_0&)
   at System.Text.Json.Schema.JsonSchema.ToJsonNode(JsonSchemaExporterOptions options)
   at System.Text.Json.Schema.JsonSchema.ToJsonNode(JsonSchemaExporterOptions options)
   at System.Text.Json.Schema.JsonSchema.ToJsonNode(JsonSchemaExporterOptions options)
   at System.Text.Json.Schema.JsonSchema.ToJsonNode(JsonSchemaExporterOptions options)
   at System.Text.Json.Schema.JsonSchemaExporter.GetJsonSchemaAsNode(JsonTypeInfo typeInfo, JsonSchemaExporterOptions exporterOptions)
   at System.Text.Json.Schema.JsonSchemaExporter.GetJsonSchemaAsNode(JsonSerializerOptions options, Type type, JsonSchemaExporterOptions exporterOptions)
   at Microsoft.AspNetCore.OpenApi.OpenApiSchemaService.CreateSchema(OpenApiSchemaKey key)
   at Microsoft.AspNetCore.OpenApi.OpenApiSchemaStore.GetOrAdd(OpenApiSchemaKey key, Func`2 valueFactory)
   at Microsoft.AspNetCore.OpenApi.OpenApiSchemaService.GetOrCreateSchemaAsync(Type type, ApiParameterDescription parameterDescription, Boolean captureSchemaByRef, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetJsonRequestBody(IList`1 supportedRequestFormats, ApiParameterDescription bodyParameter, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetRequestBodyAsync(ApiDescription description, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOperationAsync(ApiDescription description, HashSet`1 capturedTags, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOperationsAsync(IGrouping`2 descriptions, HashSet`1 capturedTags, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOpenApiPathsAsync(HashSet`1 capturedTags, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.OpenApi.OpenApiDocumentService.GetOpenApiDocumentAsync(CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Builder.OpenApiEndpointRouteBuilderExtensions.<>c__DisplayClass0_0.<<MapOpenApi>b__0>d.MoveNext()

.NET Version

9.0.100-preview.7.24407.12

Anything else?

No response

martincostello commented 3 weeks ago

Looks like these two lines of code need updating to pass through at least NumberStyles.AllowExponent through:

https://github.com/dotnet/aspnetcore/blob/48a07213d4d8df15c6dffccd161844842c196998/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs#L94-L95

mus65 commented 3 weeks ago

Couldn't this special case double and int to avoid ToString()/Parse completely? The RangeAttribute constructors only support double, int and string.

string would also fail currently, not sure if it can even be mapped to OpenAPI.

see: https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.rangeattribute?view=net-8.0

captainsafia commented 3 weeks ago

@mus65 Thanks for filing this issue!

Unfortunately, we're having to deal with a constraint in the underlying OpenApiSchema representation here. The OpenApiSchema.Minimum and OpenApiSchema.Maximum properties are decimal types. Based on the JSON schema validation spec, they are intended to only validate numeric values (see this section in the spec).

So, for the string case, the best approach would probably be to use a schema transformer to define an extension property on the spec to encode minimum and maximum-values for non-numeric types (like dates).

I suspect that long might have similar constraints given limitations on the maximum integer size supported in JavaScript/JSON? My recollection here is the recommendation is to transmit long values as strings when they need to be transmitted over the wire.

I think the best thing to do here would be to catch the exception that is thrown here and provide a clearer message about the constraints of the minimum/maximum type.

I'll mark this as something to doc for .NET 9 and try to fix the exception for .NET 10.