Skip to content

fix: wrap optional scalar reference types in Option<T> (#122)#357

Merged
sergey-tihon merged 12 commits intomasterfrom
repo-assist/fix-issue-122-wrap-nullable-strings-362a357b9d702fbf
Apr 13, 2026
Merged

fix: wrap optional scalar reference types in Option<T> (#122)#357
sergey-tihon merged 12 commits intomasterfrom
repo-assist/fix-issue-122-wrap-nullable-strings-362a357b9d702fbf

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

@github-actions github-actions bot commented Apr 6, 2026

Closes #122

Root Cause

DefinitionCompiler wraps non-required value types (e.g., bool option, int option) in Option<T>, but skips reference types entirely because they're already nullable by the CLR. This means optional string, byte[], and Stream fields come through as plain unwrapped types even though the F# type system should reflect the optional/absent distinction.

Fix

Extend the Option<T> wrapping in the v3 DefinitionCompiler from string-only to all scalar reference types: string, IO.Stream, and byte[*]. Collection types (arrays, maps) and provided object types remain unwrapped — they naturally express absence via null/empty.
This is a breaking change for consumers who previously received bare string / Stream / byte[] for optional fields; they will now receive Option<T> instead.
The v2 SwaggerClientProvider is not updated — its Obsolete attribute is strengthened to an explicit deprecation notice directing users to OpenApiClientProvider.

Changes

File Change
src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs Wrap string, IO.Stream, and byte[*] in Option<T> when non-required
src/SwaggerProvider.DesignTime/Provider.SwaggerClient.fs Strengthen Obsolete attribute to explicit deprecation notice
tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs Update byte-array test to expect Option<byte[*]>; add optional binary maps to Option<Stream> test
tests/SwaggerProvider.Tests/RuntimeHelpersTests.fs Add ByteArrayOptionClass + 3 deserialization tests for byte array option; rename module to OptionDeserializationTests

Test Status

✅ Build: succeeded (0 errors)
✅ Unit tests: 337 passed, 0 failed, 2 skipped
✅ Format check: fantomas applied and verified clean

…er (closes #122)

When WrapNullableStrings=true, optional/nullable string fields are
wrapped as Option<string> instead of plain string, matching the
treatment of other optional value types (bool, int, float, etc.).

Defaults to false to preserve backwards compatibility.

- Add WrapNullableStrings static parameter to DefinitionCompiler (optional arg)
- Pass parameter through Provider.OpenApiClient.fs to DefinitionCompiler
- Include WrapNullableStrings in the type-generation cache key
- Add compilePropertyTypeWith helper in test helpers
- Add 5 tests covering WrapNullableStrings=false/true/required scenarios

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sergey-tihon
Copy link
Copy Markdown
Member

@copilot merge develop into this branch

…ue-122-wrap-nullable-strings-362a357b9d702fbf

# Conflicts:
#	tests/SwaggerProvider.Tests/v3/Schema.TestHelpers.fs
#	tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs

Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 13, 2026

@copilot merge develop into this branch

There's no develop branch in this repo — master is the main branch. I've merged master into this branch in commit 309417a, resolving conflicts in Schema.TestHelpers.fs and Schema.TypeMappingTests.fs. All 333 unit tests pass.

@sergey-tihon
Copy link
Copy Markdown
Member

/repo-assist i don't want add WrapNullableStrings static parameters. i want to have the beharior of WrapNullableStrings=true by defaiult

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions bot commented Apr 13, 2026

✗ Repo Assist encountered failed, see workflow run.

@sergey-tihon
Copy link
Copy Markdown
Member

@copilot i don't want add WrapNullableStrings static parameters. i want to have the beharior of WrapNullableStrings=true by defaiult

…avior (remove WrapNullableStrings param)

Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/345881f5-ff34-40a2-aeba-703b53cb2dbc

Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 13, 2026

@copilot i don't want add WrapNullableStrings static parameters. i want to have the beharior of WrapNullableStrings=true by defaiult

Done in commit ae9ecdd. The WrapNullableStrings static parameter has been removed entirely. Optional string fields are now always wrapped in Option<string> by default — no opt-in needed. All 328 unit tests pass.

@sergey-tihon sergey-tihon marked this pull request as ready for review April 13, 2026 07:49
Copilot AI review requested due to automatic review settings April 13, 2026 07:49
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to address #122 by allowing nullable/optional OpenAPI string properties to be represented as Option<string> in generated client types, aligning string behavior with existing optional value-type wrapping.

Changes:

  • Updates v3 DefinitionCompiler to wrap non-required string types in Option<string>.
  • Updates v3 mapping tests to expect Option<string> for non-required strings (including in PreferNullable mode).
  • Updates the v2-compat compilation test (via v3 pipeline) to expect Option<string> for an optional string property.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/SwaggerProvider.DesignTime/v3/DefinitionCompiler.fs Adds unconditional wrapping of non-required string into Option<string>.
src/SwaggerProvider.DesignTime/Provider.OpenApiClient.fs No functional change shown in diff; still constructs DefinitionCompiler without a WrapNullableStrings toggle.
tests/SwaggerProvider.Tests/v3/Schema.TypeMappingTests.fs Changes expectations so optional strings map to Option<string>, including under PreferNullable.
tests/SwaggerProvider.Tests/v3/Schema.V2SchemaCompilationTests.fs Changes v2-compat expectation so an optional string property becomes string option.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@sergey-tihon
Copy link
Copy Markdown
Member

@copilot why don't you use Option.ofString if runtime to avoid Some(null) ?

…option setters

Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/4f7cea4d-4b04-4690-8f42-618b300354b1

Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 13, 2026

@copilot why don't you use Option.ofString if runtime to avoid Some(null) ?

Added in commit 248303e. RuntimeHelpers.sanitizeStringOption applies Option.bind Option.ofObj to convert Some(null)None. The generated property setter for string option fields now calls this function, so a CLR null string from any deserializer that doesn't understand F# options will correctly produce None instead of Some(null). Some("") is left unchanged (empty string is distinct from absent).

…behavior tests

Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/7e0210bf-5236-4b01-a81f-850ea0d1cc88

Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com>
…e v2 provider

Extend Option<T> wrapping from string-only to all scalar reference types
(string, IO.Stream, byte[*]) in the v3 DefinitionCompiler. Collection types
(arrays, maps) and provided object types remain unwrapped.

Strengthen the Obsolete attribute on SwaggerClientProvider to an explicit
deprecation notice directing users to OpenApiClientProvider.

Add byte-array Option deserialization tests and extend type mapping tests
to cover Stream and byte[] wrapping.
@sergey-tihon sergey-tihon changed the title [Repo Assist] feat: add WrapNullableStrings static parameter to OpenApiClientProvider fix: wrap optional scalar reference types in Option<T> (#122) Apr 13, 2026
…ullable=true; add tests

Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/317c7465-cd4a-4896-9c89-808d8fc38a3f

Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 13, 2026

@copilot merge develop into this branch

There's no develop branch — master is the main branch. It was merged into this branch in commit 309417a.

@copilot i don't want add WrapNullableStrings static parameters. i want to have the beharior of WrapNullableStrings=true by defaiult

Done in commit ae9ecdd — the WrapNullableStrings static parameter was removed entirely. Optional string fields are always wrapped in Option<string> by default.

@copilot why don't you use Option.ofString if runtime to avoid Some(null) ?

Verified in commit 508faf8 that sanitizeStringOption was not needed — both JsonFSharpConverter and plain System.Text.Json already map JSON nullNone for string option properties. The function was removed and replaced with StringOptionDeserializationTests that document this behavior.

@sergey-tihon
Copy link
Copy Markdown
Member

@copilot fix build

…am/byte[] fields

Agent-Logs-Url: https://github.com/fsprojects/SwaggerProvider/sessions/099ce0a4-f3ab-40d6-b12b-038f654293f3

Co-authored-by: sergey-tihon <1197905+sergey-tihon@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 13, 2026

@copilot fix build

Fixed in commit 38efd7d. The CI failures were in the provider integration tests — four test files needed to use Some values where optional string, byte[], and Stream properties/parameters are now Option<T>:

  • Swashbuckle.ReturnControllers.Tests.fsfile.Name/file.Bytes comparisons now use Some(...)
  • Swashbuckle.UpdateControllers.Tests.fs — string/enum args wrapped in Some, FileDescription constructor uses Some, byte[] comparisons use Some
  • Swashbuckle.CancellationToken.Tests.fs — string arg wrapped in Some
  • Swashbuckle.FileController.Tests.fs — formData constructors use Some for optional Stream/string params

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • petstore.swagger.io
    • Triggering command: /usr/share/dotnet/dotnet /usr/share/dotnet/dotnet /usr/share/dotnet/sdk/10.0.201/FSharp/fsc.dll @/tmp/MSBuildTempwAPLtA/tmp4972b2fd57504c608f450a24d853c413.rsp tests/Swashbuckle.WebApi.Server/Controllers/ReturnTextControllers.fs tests/Swashbuckle.WebApi.Server/Controllers/SpecialCasesControllers.fs tests/Swashbuckle.WebApi.Server/Controllers/ReturnControllers.fs (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@sergey-tihon sergey-tihon merged commit 20b0fab into master Apr 13, 2026
2 checks passed
@sergey-tihon sergey-tihon deleted the repo-assist/fix-issue-122-wrap-nullable-strings-362a357b9d702fbf branch April 13, 2026 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

String field cannot be optional.

3 participants