Skip to content

OpenAPI/Swagger Integration ​

Sannr provides seamless integration with Swashbuckle.AspNetCore, automatically converting your validation attributes into OpenAPI schema constraints at compile-time. This ensures your API documentation stays synchronized with your validation rules without any runtime reflection.

Setup ​

1. Install Dependencies ​

Ensure you have Swashbuckle.AspNetCore installed:

bash
dotnet add package Swashbuckle.AspNetCore

2. Configure SwaggerGen ​

In your Program.cs, register the Sannr schema filter:

csharp
using Sannr.OpenApi;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSwaggerGen(options =>
{
    // Register the compile-time-generated schema filter
    options.SchemaFilter<SannrGeneratedSchemaFilter>();
});

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

3. Define Your Models ​

Models must be partial for the Sannr source generator to work. Forgetting partial will produce a build error (SANN004):

csharp
using Sannr;

namespace MyApp.Models;

public partial class UserRegistrationRequest
{
    [Required, EmailAddress]
    public string Email { get; set; } = string.Empty;

    [Required, StringLength(100, MinimumLength = 2)]
    public string FirstName { get; set; } = string.Empty;

    [Range(13, 120)]
    public int Age { get; set; }

    [Url]
    public string? Website { get; set; }

    [Phone]
    public string? PhoneNumber { get; set; }

    [CreditCard]
    public string? CreditCardNumber { get; set; }

    [FileExtensions]
    public string? ProfileImagePath { get; set; }
}

That is all that is needed. The source generator automatically registers schema constraints at startup.

How It Works ​

Sannr uses a compile-time registration pattern rather than runtime reflection:

  1. Build-time: The Sannr source generator inspects your model classes and emits a static initializer ([ModuleInitializer]) that registers per-type schema appliers into SannrGeneratedSchemaFilter.
  2. App startup: The ModuleInitializer runs automatically, populating the schema applier dictionary.
  3. Request time: When Swashbuckle generates your OpenAPI spec, SannrGeneratedSchemaFilter.Apply() looks up the registered applier for the model type and applies the OpenAPI constraints.

This means:

  • Zero runtime reflection
  • Full AOT compatibility
  • SannrGeneratedSchemaFilter is always present in the assembly, regardless of whether any models have validation attributes

Supported Attributes ​

Sannr AttributeOpenAPI ConstraintExample
[EmailAddress]format: "email""email": {"type": "string", "format": "email"}
[Url]format: "uri""website": {"type": "string", "format": "uri"}
[Range(min, max)]minimum, maximum"age": {"type": "integer", "minimum": 18, "maximum": 120}
[StringLength(max, min)]minLength, maxLength"name": {"type": "string", "minLength": 2, "maxLength": 100}
[Phone]format: "tel""phone": {"type": "string", "format": "tel"}
[CreditCard]format: "credit-card""card": {"type": "string", "format": "credit-card"}
[FileExtensions]format: "file""avatar": {"type": "string", "format": "file"}

Generated OpenAPI Schema Example ​

Given the model above, Sannr produces:

json
{
  "UserRegistrationRequest": {
    "type": "object",
    "properties": {
      "email": { "type": "string", "format": "email" },
      "firstName": { "type": "string", "minLength": 2, "maxLength": 100 },
      "age": { "type": "integer", "minimum": 13, "maximum": 120 },
      "website": { "type": "string", "format": "uri" },
      "phoneNumber": { "type": "string", "format": "tel" },
      "creditCardNumber": { "type": "string", "format": "credit-card" },
      "profileImagePath": { "type": "string", "format": "file" }
    }
  }
}

Benefits ​

Single Source of Truth ​

Validation rules and API documentation are always synchronized. When you update a validation attribute, the OpenAPI schema automatically reflects the change at the next build.

AOT Compatible ​

The schema filter uses no reflection at runtime. It is safe for use in Native AOT-published applications.

Reduced Boilerplate ​

No need for manual MapType<> calls or custom schema classes per model.

Limitations ​

Required Properties ​

The [Required] attribute does not currently set the required array in OpenAPI schemas. That is handled by Swashbuckle's base functionality.

Complex and Conditional Validations ​

Attributes like [RequiredIf] and custom validators do not affect OpenAPI schemas - they represent runtime conditional logic.

Troubleshooting ​

Schema Not Generated ​

  • Ensure options.SchemaFilter<SannrGeneratedSchemaFilter>() is called in AddSwaggerGen
  • Verify the Sannr NuGet package is referenced
  • Confirm model properties have Sannr validation attributes

SANN004 Build Error: Class Must Be Partial ​

If you see error SANN004: Class 'X' has Sannr validation attributes but is not declared as 'partial', add the partial keyword to your model class. This is required for the source generator.

Incorrect Constraints ​

  • Verify attribute parameters match expected types (e.g. [StringLength(100, MinimumLength = 2)])
  • Ensure attributes are from the Sannr namespace, not System.ComponentModel.DataAnnotations

Build Errors ​

  • Confirm Swashbuckle.AspNetCore is properly referenced
  • Check for conflicting schema filter registrations

Advanced Usage ​

Combining with Custom Filters ​

csharp
builder.Services.AddSwaggerGen(options =>
{
    options.SchemaFilter<SannrGeneratedSchemaFilter>();

    // Add your own custom filters alongside Sannr
    options.SchemaFilter<MyCustomSchemaFilter>();
});

Integration with API Versioning ​

csharp
builder.Services.AddSwaggerGen(options =>
{
    options.SchemaFilter<SannrGeneratedSchemaFilter>();
    options.DocInclusionPredicate((name, api) => true);
});

Migration from Manual Schema Definitions ​

Before (Manual) ​

csharp
builder.Services.AddSwaggerGen(options =>
{
    options.MapType<UserRequest>(() => new OpenApiSchema
    {
        Type = "object",
        Properties = new Dictionary<string, OpenApiSchema>
        {
            ["email"] = new() { Type = "string", Format = "email" },
            ["age"] = new() { Type = "integer", Minimum = 18, Maximum = 120 }
        }
    });
});

After (Sannr v1.5) ​

csharp
builder.Services.AddSwaggerGen(options =>
{
    options.SchemaFilter<SannrGeneratedSchemaFilter>(); // Single line
});

// Model drives the schema automatically
public partial class UserRequest
{
    [EmailAddress] public string Email { get; set; } = string.Empty;
    [Range(18, 120)] public int Age { get; set; }
}

Testing ​

Run the included OpenAPI integration tests with:

bash
dotnet test --filter OpenApiIntegrationTests

Released under the MIT License.