JSON Schema Validation

This document describes the JSON Schema validation feature in the StellaOps Router/Microservice SDK.

Overview

The StellaOps Microservice SDK provides compile-time JSON Schema generation and runtime request/response validation. Schemas are automatically generated from your C# types using a source generator, then transmitted to the Gateway via the HELLO payload where they power both runtime validation and OpenAPI documentation.

Key Features

Benefits

BenefitDescription
Type safetyContract enforcement between clients and services
Early error detectionInvalid requests rejected with 422 before handler execution
Documentation automationNo manual schema maintenance required
InteroperabilityStandard JSON Schema works with any tooling

Quick Start

1. Add the ValidateSchema Attribute

using StellaOps.Microservice;

[StellaEndpoint("POST", "/invoices")]
[ValidateSchema]
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>
{
    public Task<CreateInvoiceResponse> HandleAsync(
        CreateInvoiceRequest request,
        CancellationToken cancellationToken)
    {
        // Request is already validated against JSON Schema
        return Task.FromResult(new CreateInvoiceResponse
        {
            InvoiceId = Guid.NewGuid().ToString(),
            CreatedAt = DateTime.UtcNow,
            Status = "draft"
        });
    }
}

2. Define Your Request/Response Types

public sealed record CreateInvoiceRequest
{
    public required string CustomerId { get; init; }
    public required decimal Amount { get; init; }
    public string? Description { get; init; }
    public List<LineItem> LineItems { get; init; } = [];
}

public sealed record CreateInvoiceResponse
{
    public required string InvoiceId { get; init; }
    public required DateTime CreatedAt { get; init; }
    public required string Status { get; init; }
}

3. Build Your Project

The source generator automatically creates JSON Schema definitions at compile time. These schemas are included in the HELLO payload when your microservice connects to the Gateway.


Attribute Reference

ValidateSchemaAttribute

Enables JSON Schema validation for an endpoint.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class ValidateSchemaAttribute : Attribute

Properties

PropertyTypeDefaultDescription
ValidateRequestbooltrueEnable request body validation
ValidateResponseboolfalseEnable response body validation
RequestSchemaResourcestring?nullEmbedded resource path to external request schema
ResponseSchemaResourcestring?nullEmbedded resource path to external response schema
Summarystring?nullOpenAPI operation summary
Descriptionstring?nullOpenAPI operation description
Tagsstring[]?nullOpenAPI tags for grouping endpoints
DeprecatedboolfalseMark endpoint as deprecated in OpenAPI

Validation Properties

ValidateRequest (default: true)

Controls whether incoming request bodies are validated against the generated schema.

// Request validation enabled (default)
[ValidateSchema(ValidateRequest = true)]

// Disable request validation
[ValidateSchema(ValidateRequest = false)]

ValidateResponse (default: false)

Enables response body validation. Useful for debugging or strict contract enforcement, but adds overhead.

// Enable response validation
[ValidateSchema(ValidateResponse = true)]

External Schema Files

Override generated schemas with embedded resource files when you need custom schema definitions:

[ValidateSchema(RequestSchemaResource = "Schemas.create-order.json")]
public sealed class CreateOrderEndpoint : IStellaEndpoint<CreateOrderRequest, CreateOrderResponse>

The schema file must be embedded as a resource in your assembly.

Documentation Properties

Summary and Description

Provide OpenAPI documentation for the operation:

[ValidateSchema(
    Summary = "Create a new invoice",
    Description = "Creates a draft invoice for the specified customer. The invoice must be finalized before it can be sent.")]
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>

Tags

Override the default service-based tag grouping:

[ValidateSchema(Tags = ["Billing", "Invoices"])]
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>

Deprecated

Mark an endpoint as deprecated in OpenAPI documentation:

[ValidateSchema(Deprecated = true, Description = "Use /v2/invoices instead")]
public sealed class CreateInvoiceV1Endpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>

Schema Discovery Endpoints

The Gateway exposes endpoints to discover and retrieve schemas from connected microservices.

Discovery Endpoint

GET /.well-known/openapi

Returns metadata about the OpenAPI document including available format URLs:

{
  "openapi_json": "/openapi.json",
  "openapi_yaml": "/openapi.yaml",
  "etag": "\"a1b2c3d4\"",
  "generated_at": "2025-01-15T10:30:00.000Z"
}

OpenAPI Document (JSON)

GET /openapi.json
Accept: application/json

Returns the full OpenAPI 3.1.0 specification in JSON format. Supports ETag-based caching:

GET /openapi.json
If-None-Match: "a1b2c3d4"

Returns 304 Not Modified if the document hasn’t changed.

OpenAPI Document (YAML)

GET /openapi.yaml
Accept: application/yaml

Returns the full OpenAPI 3.1.0 specification in YAML format.

Caching Behavior

All OpenAPI endpoints support HTTP caching:

HeaderValueDescription
Cache-Controlpublic, max-age=60Client-side caching for 60 seconds
ETag"<hash>"Content hash for conditional requests

Examples

Basic Endpoint with Validation

[StellaEndpoint("POST", "/invoices")]
[ValidateSchema]
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>
{
    private readonly ILogger<CreateInvoiceEndpoint> _logger;

    public CreateInvoiceEndpoint(ILogger<CreateInvoiceEndpoint> logger)
    {
        _logger = logger;
    }

    public Task<CreateInvoiceResponse> HandleAsync(
        CreateInvoiceRequest request,
        CancellationToken cancellationToken)
    {
        _logger.LogInformation(
            "Creating invoice for customer {CustomerId} with amount {Amount}",
            request.CustomerId,
            request.Amount);

        return Task.FromResult(new CreateInvoiceResponse
        {
            InvoiceId = $"INV-{Guid.NewGuid():N}"[..16].ToUpperInvariant(),
            CreatedAt = DateTime.UtcNow,
            Status = "draft"
        });
    }
}

Endpoint with Full Documentation

[StellaEndpoint("POST", "/invoices", TimeoutSeconds = 30)]
[ValidateSchema(
    Summary = "Create invoice",
    Description = "Creates a new draft invoice for the specified customer. Line items are optional but recommended for itemized billing.",
    Tags = ["Billing", "Invoices"])]
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>
{
    // Implementation...
}

Deprecated Endpoint

[StellaEndpoint("POST", "/v1/invoices")]
[ValidateSchema(
    Deprecated = true,
    Summary = "Create invoice (deprecated)",
    Description = "This endpoint is deprecated. Use POST /v2/invoices instead.")]
public sealed class CreateInvoiceV1Endpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>
{
    // Implementation...
}

Request with Complex Types

public sealed record CreateInvoiceRequest
{
    /// <summary>
    /// The customer identifier.
    /// </summary>
    public required string CustomerId { get; init; }

    /// <summary>
    /// The invoice amount in the default currency.
    /// </summary>
    public required decimal Amount { get; init; }

    /// <summary>
    /// Optional description for the invoice.
    /// </summary>
    public string? Description { get; init; }

    /// <summary>
    /// Line items for itemized billing.
    /// </summary>
    public List<LineItem> LineItems { get; init; } = [];
}

public sealed record LineItem
{
    public required string Description { get; init; }
    public required decimal Amount { get; init; }
    public int Quantity { get; init; } = 1;
}

The source generator produces JSON Schema that includes:


Architecture

Schema Flow Diagram

┌─────────────────────────────────────────────────────────────────────┐
│                        COMPILE TIME                                  │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  C# Types              Source Generator         Generated Code       │
│  ─────────             ────────────────         ──────────────       │
│  CreateInvoice    ──►  Analyzes types      ──►  JSON Schema defs    │
│  Request               Extracts metadata        GetSchemaDefinitions│
│  CreateInvoice         Generates schemas        EndpointDescriptor  │
│  Response                                       with SchemaInfo     │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                         RUNTIME                                      │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  Microservice Start    HELLO Payload            Gateway Storage      │
│  ─────────────────     ─────────────            ───────────────      │
│  RouterConnection  ──► Instance info        ──► ConnectionState      │
│  Manager               Endpoints[]              Schemas dictionary   │
│                        Schemas{}                OpenApiInfo          │
│                        OpenApiInfo                                   │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                      HTTP EXPOSURE                                   │
├─────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  OpenApiDocument       Document Cache           HTTP Endpoints       │
│  Generator             ─────────────            ──────────────       │
│  ──────────────                                                      │
│  Aggregates all    ──► TTL-based cache     ──► GET /openapi.json    │
│  connected services    ETag generation          GET /openapi.yaml   │
│  Prefixes schemas      Invalidation on          GET /.well-known/   │
│  by service name       connection change            openapi         │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘

Schema Naming Convention

Schemas are prefixed with the service name to avoid conflicts:

This allows multiple services to define types with the same name without collisions.


Error Handling

Validation Errors

When request validation fails, the endpoint returns a 422 Unprocessable Entity response with details:

{
  "type": "https://tools.ietf.org/html/rfc4918#section-11.2",
  "title": "Validation Error",
  "status": 422,
  "errors": [
    {
      "path": "$.customerId",
      "message": "Required property 'customerId' is missing"
    },
    {
      "path": "$.amount",
      "message": "Value must be a number"
    }
  ]
}

Invalid Schemas

If a schema cannot be parsed (e.g., malformed JSON in external schema file), the schema is skipped and a warning is logged. The endpoint will still function but without schema validation.


See Also