๐ Add OpenAPI Support
OpenAPI (Swagger) documentation makes your API discoverable, self-documenting, and easy to test. It gives frontend developers, API clients, and internal teams clear visibility into what endpoints are available and how to use them.
Potato includes helpers for auto-generating OpenAPI schemas for commands, queries, and tree-structured nodes.
๐ฆ 1. Install Required Packages
From the Potato.Examples.Application project:
dotnet add package Swashbuckle.AspNetCore.ReDoc
dotnet add package Potato.OpenApi
๐๏ธ 2. Add Schema Filters to Application Layer
Create a folder:
Potato.Example.Application/OpenApi/
And move each schema filter into its appropriate location:
NodeSchemaFilter.csโOpenApi/UserNodeSchemaFilter.csโOpenApi/CreateUserCommand.SchemaFilter.csโFeatures/Users/Create/SearchUsersQuery.SchemaFilter.csโFeatures/Users/Search/
Then create or update the filters as shown:
๐งพ NodeSchemaFilter.cs
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Potato.Examples.Application;
/// <summary>
/// Adds OpenAPI metadata for the <see cref="Node"/> schema and all types that inherit from it.
/// This includes discriminator mapping, required properties, and type identification.
/// </summary>
public class NodeSchemaFilter : PotatoBaseSchemaFilter
{
/// <summary>
/// Applies custom schema filters to Node and derived types.
/// Adds <c>$type</c> discriminator, marks required fields, and injects base schema documentation.
/// </summary>
public override void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
base.Apply(schema, context);
// Apply to all types derived from Node
if (context.Type.IsAssignableTo(typeof(Node)))
{
PotatoOpenApiSchemaBuilder<Node>
.For(schema, context)
.HasDescriminator()
.Required(p => p.Path)
.Required(p => p.DisplayName);
}
// Apply base type documentation and discriminator
if (context.Type == typeof(Node))
{
PotatoOpenApiSchemaBuilder<Node>
.For(schema, context)
.Description("Represents a base node in a hierarchical structure.")
.WithDiscriminator();
}
}
}
๐งพ CreateUserCommand.SchemaFilter.cs
using Microsoft.OpenApi.Models;
using Potato.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Potato.Examples.Application;
public class CreateUserCommandSchemaFilter : PotatoBaseSchemaFilter
{
public override void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (context.Type == typeof(CreateUserCommand))
{
PotatoOpenApiSchemaBuilder<CreateUserCommand>
.For(schema, context)
.Description("Creates a new user node.")
.Required(p => p.Username)
.NotNullable(p => p.Username);
}
}
}
๐ SearchUsersQuery.SchemaFilter.cs
using Microsoft.OpenApi.Models;
using Potato.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Potato.Examples.Application;
public class SearchUsersQuerySchemaFilter : PotatoBaseSchemaFilter
{
public override void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (context.Type == typeof(SearchUsersQuery))
{
PotatoOpenApiSchemaBuilder<SearchUsersQuery>
.For(schema, context)
.Example(new SearchUsersQuery(
Filter: PotatoFilter.OfType<UserNode>(),
Cursor: PotatoCursor.ForwardPagination(10),
Sort: PotatoSort.Ascending(nameof(UserNode.Username))
));
}
}
}
๐งฉ 3. Create an Extension to Register Schema Filters
Create a file OpenApi/OpenApiExtensions.cs:
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Microsoft.Extensions.DependencyInjection;
public static class OpenApiExtensions
{
public static void AddExampleSchemaFilters(this SwaggerGenOptions options)
{
options.SchemaFilter<NodeSchemaFilter>();
options.SchemaFilter<UserNodeSchemaFilter>();
options.SchemaFilter<CreateUserCommandSchemaFilter>();
options.SchemaFilter<SearchUsersQuerySchemaFilter>();
}
}
๐ง 4. Understanding Discriminators in Node Trees
When working with polymorphic types like Node, UserNode, and UserGroupNode, Potato adds a $type discriminator to your OpenAPI schema.
This allows tools like ReDoc and Swagger UI to understand which concrete types belong to the Node base type. You can:
- Filter by type in clients (e.g.,
UserNodeonly) - Use autocomplete for known derived types
โ
Potato.OpenApi handles discriminator registration automatically for any type derived from Node.
โ๏ธ 5. Wire It Up in Program.cs
Make sure all your endpoints call .ProducesDefault<T>() so OpenAPI can generate the correct response schema. For example:
app.MapPut("/users/create", ...).ProducesDefault<UserNode>();
app.MapPut("/users/search", ...).ProducesDefault<PotatoEdgeCollection<UserNode>>();
Then register your filters and UI:
Add to your SwaggerGen setup:
builder.Services
.AddEndpointsApiExplorer()
.AddSwaggerGen(options =>
{
options.AddPotatoSchemaFilters();
options.AddEdgeCollectionSchemaFilters<Node>();
options.AddCollectionSegmentSchemaFilters<Node>();
options.AddExampleSchemaFilters();
});
app.UseSwagger();
app.UseReDoc(options =>
{
options.RoutePrefix = "docs";
options.DocumentTitle = "Potato API";
options.SpecUrl = "/swagger/v1/swagger.json";
});
โ Summary
You now have:
- Swagger + ReDoc working with minimal API
- Schema filters for your commands and queries
- Proper discriminator support for tree-based nodes
โก๏ธ Next: define a query for finding nodes generically, or start documenting RAG endpoints.