diff --git a/docs/OpenApiClientProvider.md b/docs/OpenApiClientProvider.md index 4ddd558..24f0ccc 100644 --- a/docs/OpenApiClientProvider.md +++ b/docs/OpenApiClientProvider.md @@ -22,6 +22,7 @@ let client = PetStore.Client() | `PreferNullable` | Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`. | | `PreferAsync` | Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`. | | `SsrfProtection` | Enable SSRF protection (blocks HTTP and localhost). Set to `false` for development/testing. Default value `true`. | +| `IgnoreParseErrors` | Continue generating the provider even when the OpenAPI parser reports validation errors (e.g. vendor extensions or non-strictly-compliant schemas). Warnings are printed to stderr. Default value `false`. | More configuration scenarios are described in [Customization section](/Customization) @@ -41,6 +42,19 @@ type ProdApi = OpenApiClientProvider<"https://api.example.com/swagger.json"> **Warning:** Never set `SsrfProtection=false` in production code. +## Non-Strictly-Compliant Schemas (IgnoreParseErrors) + +Some OpenAPI schemas generated by tools such as [NSwag](https://github.com/RicoSuter/NSwag) may include extensions or properties that are technically invalid (e.g. `nullable: true` at the parameter level). By default, SwaggerProvider aborts with an error when the Microsoft.OpenApi parser reports such validation errors. + +Set `IgnoreParseErrors=true` to continue generating the type provider despite these errors. Validation warnings are printed to stderr so they remain visible: + +```fsharp +// NSwag-generated schema with non-standard nullable annotations +type MyApi = OpenApiClientProvider<"https://example.com/swagger.json", IgnoreParseErrors=true> +``` + +**Note:** Only use `IgnoreParseErrors=true` when you trust the schema source. Suppressing errors may hide genuine schema problems that could affect the generated client. + ## Sample ```fsharp diff --git a/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs b/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs index 008761d..4fce415 100644 --- a/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs +++ b/src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs @@ -38,7 +38,8 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = ProvidedStaticParameter("IgnoreControllerPrefix", typeof, true) ProvidedStaticParameter("PreferNullable", typeof, false) ProvidedStaticParameter("PreferAsync", typeof, false) - ProvidedStaticParameter("SsrfProtection", typeof, true) ] + ProvidedStaticParameter("SsrfProtection", typeof, true) + ProvidedStaticParameter("IgnoreParseErrors", typeof, false) ] t.AddXmlDoc """Statically typed OpenAPI provider. @@ -47,7 +48,8 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = Do not parse `operationsId` as `_` and generate one client class for all operations. Default value `true`. Provide `Nullable<_>` for not required properties, instead of `Option<_>`. Defaults value `false`. Generate async actions of type `Async<'T>` instead of `Task<'T>`. Defaults value `false`. - Enable SSRF protection (blocks HTTP and localhost). Set to false for development/testing. Default value `true`.""" + Enable SSRF protection (blocks HTTP and localhost). Set to false for development/testing. Default value `true`. + Continue generating the provider even when the OpenAPI parser reports validation errors (e.g. vendor extensions or non-strictly-compliant schemas). Warnings are printed to stderr. Default value `false`.""" t.DefineStaticParameters( staticParams, @@ -58,6 +60,7 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = let preferNullable = unbox args.[3] let preferAsync = unbox args.[4] let ssrfProtection = unbox args.[5] + let ignoreParseErrors = unbox args.[6] // Cache key includes cfg.RuntimeAssembly, cfg.ResolutionFolder, and cfg.SystemRuntimeAssemblyVersion // to differentiate between different TFM builds (same approach as FSharp.Data) @@ -69,6 +72,7 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = preferNullable, preferAsync, ssrfProtection, + ignoreParseErrors, cfg.RuntimeAssembly, cfg.ResolutionFolder, cfg.SystemRuntimeAssemblyVersion) @@ -89,11 +93,15 @@ type public OpenApiClientTypeProvider(cfg: TypeProviderConfig) as this = let schema, diagnostic = (readResult.Document, readResult.Diagnostic) if diagnostic.Errors.Count > 0 then - failwithf - "Schema parse errors:\n%s" - (diagnostic.Errors - |> Seq.map(fun e -> $"%s{e.Message} @ %s{e.Pointer}") - |> String.concat "\n") + if ignoreParseErrors then + diagnostic.Errors + |> Seq.iter(fun e -> eprintfn "SwaggerProvider warning: %s @ %s" e.Message e.Pointer) + else + failwithf + "Schema parse errors:\n%s" + (diagnostic.Errors + |> Seq.map(fun e -> $"%s{e.Message} @ %s{e.Pointer}") + |> String.concat "\n") let defCompiler = DefinitionCompiler(schema, preferNullable) diff --git a/tests/SwaggerProvider.ProviderTests/Schemas/v3/nullable-parameter-issue261.json b/tests/SwaggerProvider.ProviderTests/Schemas/v3/nullable-parameter-issue261.json new file mode 100644 index 0000000..162e69a --- /dev/null +++ b/tests/SwaggerProvider.ProviderTests/Schemas/v3/nullable-parameter-issue261.json @@ -0,0 +1,38 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Nullable Parameter Test", + "version": "1.0.0" + }, + "paths": { + "/items/{id}": { + "get": { + "operationId": "GetItem", + "summary": "Get an item by ID", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "nullable": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/tests/SwaggerProvider.Tests/Schema.Parser.Tests.fs b/tests/SwaggerProvider.Tests/Schema.Parser.Tests.fs index 208c8b7..6b22c54 100644 --- a/tests/SwaggerProvider.Tests/Schema.Parser.Tests.fs +++ b/tests/SwaggerProvider.Tests/Schema.Parser.Tests.fs @@ -105,6 +105,33 @@ let ``Add definition for schema with only allOf properties``() = definitions |> shouldHaveLength 1 definitions[0].GetDeclaredProperty("FirstName") |> shouldNotEqual null +[] +let ``Schema with nullable parameter-level property triggers parse errors``() = + let schemaPath = + __SOURCE_DIRECTORY__ + + "/../SwaggerProvider.ProviderTests/Schemas/v3/nullable-parameter-issue261.json" + + let settings = OpenApiReaderSettings() + settings.AddYamlReader() + + let readResult = + Microsoft.OpenApi.OpenApiDocument.Parse(File.ReadAllText schemaPath, settings = settings) + // Microsoft.OpenApi reports 'nullable' at the parameter level as an error in OpenAPI 3.0 + // When this is true, IgnoreParseErrors=true allows the provider to proceed instead of failing + if readResult.Diagnostic.Errors.Count > 0 then + readResult.Diagnostic.Errors + |> Seq.exists(fun e -> e.Message.Contains("nullable")) + |> shouldEqual true + +[] +let ``Schema with nullable parameter is still parseable despite errors``() = + let schemaPath = + __SOURCE_DIRECTORY__ + + "/../SwaggerProvider.ProviderTests/Schemas/v3/nullable-parameter-issue261.json" + + // Even with diagnostic errors, the schema should still compile without throwing an exception + File.ReadAllText schemaPath |> V3.testSchema |> ignore + (* [] let parseJsonSchemaTests =