ASP.NET Core OpenAPI enum defaults can describe the wrong contract when a request record gives an enum parameter a constructor default. With shared enum schemas, the default can move onto the reusable enum component, making unrelated request properties appear to have the same default. In the reported regression, reversing endpoint registration order can also decide whether that default is present at all.

This is not cosmetic metadata if generated clients, contract snapshots, or API reviewers depend on the document. This guide reproduces the behavior on Microsoft.AspNetCore.OpenApi 10.0.13, applies the supported enum-inlining hook as a contained mitigation, and adds an order-invariance check so the workaround cannot quietly regress.

Recognize the contract corruption

Start with two request records that share one enum but do not share one default:

using System.Text.Json.Serialization;

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Priority
{
    Low,
    Normal,
    High
}

public sealed record CreateTicketRequest(
    string Title,
    Priority Priority = Priority.Normal);

public sealed record UpdateTicketRequest(
    string Title,
    Priority Priority);

The intended contract is property-specific: CreateTicketRequest.priority has a default of Normal; UpdateTicketRequest.priority has no default. A shared Priority component should describe the enum values, not inherit one constructor’s default.

The affected output instead places default: Normal on the shared enum schema. Both request properties reference that schema, so tools that resolve the reference can conclude that both properties have the same default. The upstream report also demonstrates an order dependency: map the endpoint without a default first and the shared component may be created without a default; map the defaulted endpoint first and the default may leak into it.

If swapping two MapPost calls changes the generated OpenAPI document, treat the document as unstable even when both endpoints still run correctly.

Reproduce the 10.0.13 behavior

Use a small Minimal API and pin the package version. The reproduction used .NET SDK 10.0.401, .NET runtime 10.0.12, and Microsoft.AspNetCore.OpenApi 10.0.13 on Linux x64.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.OpenApi"
                      Version="10.0.13" />
  </ItemGroup>
</Project>
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOpenApi();

var app = builder.Build();
app.MapOpenApi();

app.MapPost("/tickets", (CreateTicketRequest request) => Results.Ok());
app.MapPut("/tickets/{id:int}",
    (int id, UpdateTicketRequest request) => Results.NoContent());

app.Run();

Generate or fetch the document, then inspect three locations rather than searching for the word default globally:

  1. components.schemas.Priority.default
  2. components.schemas.CreateTicketRequest.properties.priority.default
  3. components.schemas.UpdateTicketRequest.properties.priority.default

In the local control run, the shared Priority component contained Normal. Both request properties referenced that component. That is the failure signal: a constructor default that belongs to one property has escaped into shared type metadata.

Keep this check separate from other OpenAPI changes. Environment-specific generation can create a different kind of drift; if you also generate documents at build time, use an explicit environment gate such as the one described in this build-time OpenAPI environment guide.

Keep ASP.NET Core OpenAPI enum defaults property-local

ASP.NET Core exposes OpenApiOptions.CreateSchemaReferenceId to control whether a type receives a reusable component reference. Returning null for enums keeps each enum schema at its declaring property, so ASP.NET Core OpenAPI enum defaults cannot attach to a shared enum component.

using Microsoft.AspNetCore.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi(options =>
{
    options.CreateSchemaReferenceId = jsonTypeInfo =>
        jsonTypeInfo.Type.IsEnum
            ? null
            : OpenApiOptions.CreateDefaultSchemaReferenceId(jsonTypeInfo);
});

The local mitigation run produced the intended shape:

LocationExpected resultVerified 10.0.13 result
CreateTicketRequest.priorityInline enum with default: NormalPass
UpdateTicketRequest.priorityInline enum without a defaultPass
components.schemas.PriorityAbsentPass

This uses a documented extension point rather than mutating the generated document after the fact. It is still a workaround, not proof that the underlying regression is fixed. Keep the package pin and contract assertions until a released package can pass the same test without enum inlining.

Make endpoint order a contract test

A single golden file can miss the most important symptom because it records only one registration order. Generate two documents from the same endpoints: once with the defaulted request mapped first and once with it mapped second. Then assert both the semantic properties and the normalized document equality.

[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task Enum_defaults_stay_on_the_declaring_property(
    bool mapCreateFirst)
{
    await using var app = await OpenApiFixture.StartAsync(mapCreateFirst);
    using var document = await app.GetOpenApiDocumentAsync();

    var createPriority = document
        .GetProperty("components").GetProperty("schemas")
        .GetProperty("CreateTicketRequest")
        .GetProperty("properties").GetProperty("priority");

    var updatePriority = document
        .GetProperty("components").GetProperty("schemas")
        .GetProperty("UpdateTicketRequest")
        .GetProperty("properties").GetProperty("priority");

    Assert.Equal("Normal", createPriority.GetProperty("default").GetString());
    Assert.False(updatePriority.TryGetProperty("default", out _));

    var schemas = document
        .GetProperty("components").GetProperty("schemas");
    Assert.False(schemas.TryGetProperty("Priority", out _));
}

The fixture can host the app with WebApplicationFactory, start a test server, or invoke your existing build-time document generator. The important part is not the harness: it is that both endpoint orders execute the same three assertions. If you also snapshot the full document, normalize volatile fields such as server URLs before comparing files.

Run this gate whenever you update the .NET SDK, Microsoft.AspNetCore.OpenApi, JSON options, or request-model constructors. A generated-client diff is useful evidence, but it should complement the focused assertions rather than replace them.

Understand the compatibility trade-off

Inlining enums changes the OpenAPI document shape. Before the workaround, a property may contain a $ref to #/components/schemas/Priority. Afterward, each property contains its own enum schema. The allowed values remain equivalent, but document consumers do not always treat those shapes as interchangeable.

  • Generated clients: some generators deduplicate inline enums; others create property-specific types.
  • Contract snapshots: expect a broad but intentional diff across every enum property, not only the affected request.
  • Custom transformers: code that searches components.schemas for enums may stop seeing them.
  • Documentation portals: repeated inline values can make the rendered schema larger.

Because the delegate applies to all enum types handled by that OpenAPI registration, do not present it as a zero-impact patch. Review the full document and regenerate at least one representative client before deployment. If changing every enum is unacceptable, hold the rollout and keep the issue under investigation rather than inventing an undocumented post-processor that could lose other schema metadata.

Roll out and remove the mitigation

Use a staged compatibility workflow:

  1. Pin the failing package. Save the package lock or restore graph with the control output.
  2. Add the two-order assertions. Confirm that the unmitigated build fails for the reason you expect.
  3. Enable enum inlining. Re-run the semantic assertions, full OpenAPI diff, and representative client generation.
  4. Publish the contract deliberately. Treat the shared-to-inline schema change according to your API governance process.
  5. Track a released fix. An open issue, merged commit, or preview build is not a released fix for your production dependency.
  6. Test removal on an upgrade branch. Remove the delegate and require both endpoint orders to pass before accepting the shared schema again.

Do not remove the assertions with the workaround. The assertions describe the contract you need; the delegate is only one implementation. They should remain after the platform fix so a future serializer or schema-generation change cannot reintroduce order-sensitive metadata.

Know the evidence limits

The executed proof covers Microsoft.AspNetCore.OpenApi 10.0.13 with record constructor defaults, string enums, two request schemas, and the documented CreateSchemaReferenceId hook. It proves the shared-component leak in that setup and proves that enum inlining restores property-local defaults there.

It does not claim that every OpenAPI library, controller application, schema transformer, or future ASP.NET Core version has the same behavior. The upstream issue was still open during verification, and no released fixed package is claimed here. Re-run the focused contract test against the exact SDK, runtime, package, JSON options, and generator used by your build.

References

Found this useful? Support more practical developer content.

Author

Practical .NET, Angular, Azure, Blazor, and AI engineering for real-world development.

Write A Comment

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
100% Free SEO Tools - Tool Kits PRO