Table of Contents

๐Ÿ“˜ 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., UserNode only)
  • 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.