Skip to content

ADR-008: OpenAPI 3.0 Specification Generation

Status

Proposed - January 2025

Future enhancement to complement custom SDK generation (ADR-007) with industry-standard API documentation.

Context

Current State

LBS Foundry has implemented a custom SDK generation system (ADR-007) that creates TypeScript and Python clients from C# queries and commands. This provides excellent developer experience for our primary client languages but has some limitations:

Current SDK Generation (ADR-007): - Best-in-class TypeScript/Python SDKs with custom builder patterns - Auto-generated from C# [DataContract] attributes - Optimized bundle sizes with tree-shaking support - Published to npm and GitHub Releases - Limited to 2 languages (TypeScript, Python) - No interactive API documentation - No standard tooling support (Postman, Swagger UI) - No contract testing/validation infrastructure

Problem Statement

While our custom SDKs provide excellent DX, we lack:

  1. Interactive Documentation - No Swagger UI or similar tools for exploring the API
  2. Multi-Language Support - Cannot easily generate clients for other languages (C#, Go, Ruby, etc.)
  3. API Testing Tools - Cannot import into Postman/Insomnia for manual testing
  4. Contract Validation - No standard way to validate requests or catch breaking changes
  5. Industry Standards - Not using OpenAPI 3.0, the industry standard for REST APIs

API Structure

Our CQRS API exposes two polymorphic endpoints:

POST /api/readmodel - Accepts any query with discriminator "queryType"
POST /api/command   - Accepts any command with discriminator "commandType"

Authentication: - Bearer JWT (Clerk) for user authentication - Basic Auth for service accounts - Role-based authorization via [RequiresRoles] attributes

Example Query:

[DataContract(Name = "GetGroups", Namespace = LbsNamespace.SportQueries)]
public class GetGroupsQuery : ICacheableQuery, IRequireUserContext
{
    public string QueryType => this.GetType().GetQueryTypeName();
    public required Sport Sport { get; set; }
    public string? Series { get; set; } = "premiership";
    public int? Season { get; set; }
}

Decision

We will implement an OpenAPI 3.0 specification generator that complements (not replaces) our custom SDK generation:

Dual Approach Strategy

  1. Keep Custom SDKs (TypeScript/Python) - Better DX, optimized bundles, custom builder patterns
  2. Add OpenAPI Spec - Interactive docs, multi-language support, testing tools

This gives us the best of both worlds: - Optimized custom SDKs for primary clients - Standard OpenAPI for documentation and other languages

Architecture

Reuse the existing TypeScanner infrastructure from ADR-007 to generate OpenAPI schemas:

┌─────────────────────────────────────────────────────┐
│         LBS.Tools.OpenApiGenerator                   │
│                                                      │
│  ┌──────────────┐  ┌──────────────┐               │
│  │ TypeScanner  │→ │ OpenApiBuilder│               │
│  │ (Existing)   │  │  (New)        │               │
│  └──────────────┘  └──────────────┘               │
└──────────────────────┬──────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│              openapi.json / openapi.yaml             │
│                                                      │
│  paths:                                             │
│    /api/readmodel:                                  │
│      post:                                          │
│        requestBody:                                 │
│          oneOf:                                     │
│            - $ref: '#/components/schemas/GetGroups' │
│            - $ref: '#/components/schemas/PlayerSearch'│
│        responses:                                   │
│          200:                                       │
│            content:                                 │
│              application/json:                      │
│                oneOf:                               │
│                  - $ref: '#/components/schemas/GroupsViewModel'│
│                  - $ref: '#/components/schemas/PlayerSearchResult'│
└─────────────────────────────────────────────────────┘

Key Components

1. OpenApiBuilder - Constructs OpenAPI document structure

public class OpenApiBuilder
{
    public OpenApiDocument Build(TypeScanResult scanResult)
    {
        var doc = new OpenApiDocument
        {
            Info = new OpenApiInfo
            {
                Title = "LBS Foundry API",
                Version = "v1",
                Description = "CQRS API for LBS Foundry Platform"
            }
        };

        AddSchemas(doc, scanResult);
        AddReadModelEndpoint(doc, scanResult);
        AddCommandEndpoint(doc, scanResult);
        AddSecuritySchemes(doc);

        return doc;
    }
}

2. SchemaGenerator - Converts ScannedType → OpenAPI schema

public class SchemaGenerator
{
    public OpenApiSchema GenerateSchema(ScannedType type)
    {
        var schema = new OpenApiSchema
        {
            Type = "object",
            Properties = new Dictionary<string, OpenApiSchema>()
        };

        // Add discriminator property
        schema.Properties["queryType"] = new OpenApiSchema
        {
            Type = "string",
            Enum = new List<IOpenApiAny> { new OpenApiString(type.Name) }
        };

        // Add all properties
        foreach (var prop in type.Properties)
        {
            schema.Properties[ToCamelCase(prop.Name)] = MapToOpenApiType(prop);
        }

        schema.Required = GetRequiredProperties(type);
        return schema;
    }
}

3. PathItemGenerator - Generates endpoint definitions with discriminator support

Generated Output

{
  "openapi": "3.0.1",
  "info": {
    "title": "LBS Foundry API",
    "version": "v1"
  },
  "paths": {
    "/api/readmodel": {
      "post": {
        "tags": ["Queries"],
        "summary": "Execute a read model query",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "oneOf": [
                  { "$ref": "#/components/schemas/GetGroups" },
                  { "$ref": "#/components/schemas/PlayerSearch" }
                ],
                "discriminator": {
                  "propertyName": "queryType",
                  "mapping": {
                    "GetGroups": "#/components/schemas/GetGroups",
                    "PlayerSearch": "#/components/schemas/PlayerSearch"
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "GetGroups": {
        "type": "object",
        "required": ["queryType", "sport"],
        "properties": {
          "queryType": { "type": "string", "enum": ["GetGroups"] },
          "sport": { "type": "string" },
          "series": { "type": "string", "nullable": true },
          "season": { "type": "integer", "nullable": true }
        },
        "example": {
          "queryType": "GetGroups",
          "sport": "NRL",
          "series": "premiership",
          "season": 2025
        }
      }
    },
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "Clerk JWT token"
      },
      "BasicAuth": {
        "type": "http",
        "scheme": "basic",
        "description": "Service account credentials"
      }
    }
  }
}

Consequences

Positive

  1. Interactive Documentation - Swagger UI for exploring and testing the API
  2. Multi-Language Support - Generate clients for 50+ languages using OpenAPI Generator
  3. Testing Tools Integration - Import into Postman, Insomnia, or generate mock servers
  4. Contract Validation - Validate requests and catch breaking changes in CI
  5. Industry Standards - Using OpenAPI 3.0, widely recognized and supported
  6. Reuses Infrastructure - Leverages existing TypeScanner from ADR-007
  7. Complements SDKs - Keeps optimized custom SDKs while adding standard docs

Negative

  1. Additional Maintenance - One more output to maintain (though auto-generated)
  2. Generic Clients - OpenAPI-generated clients are larger and less optimized than custom SDKs
  3. Development Effort - Requires 4-6 days to implement fully
  4. CI/CD Changes - Need to add OpenAPI generation to build pipeline
  5. Learning Curve - Team needs to understand OpenAPI spec format

Risks

  1. Breaking Changes - Need clear versioning strategy for OpenAPI spec
  2. Discriminator Complexity - Polymorphic endpoints with oneOf can be harder to document
  3. Maintenance Drift - OpenAPI spec could drift from actual API if not properly tested

Implementation

Phase 1: Core Generator (2-3 days)

File Structure:

src/Tools/LBS.Tools.OpenApiGenerator/
├── Program.cs                  # CLI entry point
├── OpenApiBuilder.cs           # Builds OpenAPI document
├── SchemaGenerator.cs          # Converts ScannedType → OpenAPI schema
├── PathItemGenerator.cs        # Generates path definitions
└── LBS.Tools.OpenApiGenerator.csproj

Phase 2: Enhanced Features (1-2 days)

  • Add authentication/authorization documentation
  • Add role-based security requirements
  • Add examples for each schema
  • Link queries to their result types

Phase 3: CI/CD Integration (0.5 days)

Add to .github/workflows/Build-Container-Apps.yml:

- name: Generate OpenAPI Specification
  run: |
    dotnet run --project src/Tools/LBS.Tools.OpenApiGenerator/LBS.Tools.OpenApiGenerator.csproj -- \
      "src/Apps/Ballr.WebApi/bin/Release/net10.0" \
      "/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/10.0.0/ref/net10.0" \
      "docs/openapi.json" \
      --format json

- name: Upload OpenAPI Spec
  uses: actions/upload-artifact@v4
  with:
    name: openapi-spec
    path: docs/openapi.json

Phase 4: Documentation (0.5 days)

  • Document usage in developer guide
  • Add examples for client generation
  • Document Swagger UI integration

Total Effort: 4-6 days

Usage

Generate OpenAPI Spec:

# Generate JSON
dotnet run --project src/Tools/LBS.Tools.OpenApiGenerator -- \
  "src/Apps/Ballr.WebApi/bin/Release/net10.0" \
  "/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/10.0.0/ref/net10.0" \
  "docs/openapi.json" \
  --format json

# Generate YAML
dotnet run --project src/Tools/LBS.Tools.OpenApiGenerator -- \
  "src/Apps/Ballr.WebApi/bin/Release/net10.0" \
  "/usr/share/dotnet/packs/Microsoft.NETCore.App.Ref/10.0.0/ref/net10.0" \
  "docs/openapi.yaml" \
  --format yaml

Generate Clients:

# Generate TypeScript client (alternative to custom SDK)
npx @openapitools/openapi-generator-cli generate \
  -i docs/openapi.json \
  -g typescript-axios \
  -o clients/typescript

# Generate Python client (alternative to custom SDK)
openapi-generator-cli generate \
  -i docs/openapi.json \
  -g python \
  -o clients/python

# Generate C# client (new capability)
openapi-generator-cli generate \
  -i docs/openapi.json \
  -g csharp-netcore \
  -o clients/csharp

Comparison Table

Feature Custom SDK (Current) OpenAPI + Generated Client
Type Safety Excellent Excellent
Maintenance Low (auto-generated) Low (auto-generated)
Language Support 2 (TS, Python) 50+ languages
Documentation SDK README Interactive API docs
Testing Tools Manual Postman, Swagger UI
Standards Custom OpenAPI 3.0 standard
Bundle Size Optimized Larger (generic client)
Builder Pattern Custom helpers Generic request builders

References

Internal Documentation

External Resources

  • Postman - API testing with OpenAPI import
  • Insomnia - REST client with OpenAPI support
  • Pact - Contract testing framework

Priority: Lower - Consider after current SDK work (ADR-007) is stable and mature.

Next Steps (when ready): 1. Create LBS.Tools.OpenApiGenerator project 2. Reuse TypeScanner from SDK generator 3. Build OpenApiBuilder and SchemaGenerator 4. Test with existing queries/commands 5. Add to CI/CD pipeline 6. Publish to docs/openapi.json