Skip to content

fix(csharp): make generichost oneOf constructors public#24341

Open
KannaKim wants to merge 1 commit into
OpenAPITools:masterfrom
KannaKim:fix-23046
Open

fix(csharp): make generichost oneOf constructors public#24341
KannaKim wants to merge 1 commit into
OpenAPITools:masterfrom
KannaKim:fix-23046

Conversation

@KannaKim

@KannaKim KannaKim commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes #23046.

The C# Generic Host template previously used model mutability (x-model-is-mutable) to determine the visibility of oneOf constructors. However, mutability does not reliably indicate whether callers need to construct a model: an immutable oneOf wrapper may still be used as an API input.

This change adds the codegen vendor extension x-model-is-operation-input. It identifies models reachable from operation parameters or request bodies, including through:

  • referenced and composed schemas (oneOf, allOf, and anyOf)
  • object properties
  • array items
  • schema-valued additionalProperties
  • parameter and request-body media-type content

Note: Read-only schema branches are excluded.

For a oneOf constructor, the template first checks x-model-is-operation-input:

  • If the flag is present, the constructor uses the configured visibility:
    public by default or internal when nonPublicApi=true.
  • If the flag is absent, the constructor is always internal.

Example

Given this OpenAPI document:

openapi: 3.0.3
info:
  title: Upload API
  version: 1.0.0

paths:
  /upload:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InitiateUploadRequest'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResponseOnlyValue'

components:
  schemas:
    InitiateUploadRequest:
      type: object
      properties:
        dynamicMetadata:
          type: object
          additionalProperties:
            $ref: '#/components/schemas/DynamicMetadataValue'

    DynamicMetadataValue:
      oneOf:
        - type: string
        - type: number

    ResponseOnlyValue:
      oneOf:
        - type: string
        - type: integer

DynamicMetadataValue is reachable from the /upload request body through InitiateUploadRequest.dynamicMetadata.additionalProperties. Its constructors therefore use the configured visibility, which is public by default:

public DynamicMetadataValue(string value)
public DynamicMetadataValue(decimal value)

ResponseOnlyValue is referenced only by the response. It is not an operation input, so its constructors remain internal:

internal ResponseOnlyValue(string value)
internal ResponseOnlyValue(int value)

Compatibility note

Note that this change may alter the generated API surface for some oneOf models. Constructors that were previously public because the model was considered mutable will now be internal when the model is only used in responses or is not reachable from an operation input.

This is intentional, but consumers that manually construct response-only oneOf models may need to update their code.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.
    @mandrean (2017/08) @shibayan (2020/02) @Blackclaws (2021/03) @lucamazzanti (2021/05) @iBicha (2023/07)

Summary by cubic

Make C# Generic Host oneOf model constructors public only for operation inputs (request bodies and parameters). Fixes #23046 so immutable oneOf inputs are constructible across assemblies by default, while non-input oneOf constructors remain internal.

  • Bug Fixes
    • Emit {{>visibility}} for Generic Host oneOf constructors and set to public only when the model is an operation input; otherwise internal. Respects nonPublicApi=true.
    • Detect operation input models in the generator (x-model-is-operation-input); updated template and codegen, added unit test and minimal OpenAPI fixture, and regenerated samples.

Written for commit c312925. Summary will update on new commits.

Review in cubic

@KannaKim
KannaKim marked this pull request as ready for review July 18, 2026 09:11

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 40 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

@devhl-labs

Copy link
Copy Markdown
Contributor

The reasoning behind the current behavior is that if none of the properties have public setters, we infer that the model is only ever received from the server and never constructed by the caller, so an internal constructor is sufficient. However, this inference breaks down for oneOf wrapper types, which can be immutable by design yet still appear as request body inputs. The correct fix would be to determine constructor visibility based on whether the model actually appears as a request type in the spec, rather than inferring it from property mutability. The current PR uses {{>visibility}} uniformly, which makes constructors public on response-only models unnecessarily.

@KannaKim
KannaKim marked this pull request as draft July 19, 2026 09:35
@KannaKim

Copy link
Copy Markdown
Contributor Author

The reasoning behind the current behavior is that if none of the properties have public setters, we infer that the model is only ever received from the server and never constructed by the caller, so an internal constructor is sufficient. However, this inference breaks down for oneOf wrapper types, which can be immutable by design yet still appear as request body inputs. The correct fix would be to determine constructor visibility based on whether the model actually appears as a request type in the spec, rather than inferring it from property mutability. The current PR uses {{>visibility}} uniformly, which makes constructors public on response-only models unnecessarily.

Good point, thanks for raising it. I added an x-model-is-operation-input vendor extension that marks models reachable from operation parameters or request bodies, including nested and composed schemas. The Generic Host Mustache template uses this flag to make the oneOf constructor public by default. Otherwise, the constructor remains internal because client users do not need to construct response-only or unused models.

@KannaKim
KannaKim marked this pull request as ready for review July 19, 2026 10:27
@KannaKim
KannaKim marked this pull request as draft July 19, 2026 10:31
@KannaKim
KannaKim marked this pull request as ready for review July 19, 2026 10:31

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 191 files

Re-trigger cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG][csharp] OneOf model constructors generated as internal, inaccessible from consuming projects

2 participants