Files
git.stella-ops.org/docs/modules/router/schema-validation.md
master cc69d332e3
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Add unit tests for RabbitMq and Udp transport servers and clients
- Implemented comprehensive unit tests for RabbitMqTransportServer, covering constructor, disposal, connection management, event handlers, and exception handling.
- Added configuration tests for RabbitMqTransportServer to validate SSL, durable queues, auto-recovery, and custom virtual host options.
- Created unit tests for UdpFrameProtocol, including frame parsing and serialization, header size validation, and round-trip data preservation.
- Developed tests for UdpTransportClient, focusing on connection handling, event subscriptions, and exception scenarios.
- Established tests for UdpTransportServer, ensuring proper start/stop behavior, connection state management, and event handling.
- Included tests for UdpTransportOptions to verify default values and modification capabilities.
- Enhanced service registration tests for Udp transport services in the dependency injection container.
2025-12-05 19:01:12 +02:00

15 KiB

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

  • Compile-time schema generation: JSON Schema draft 2020-12 generated from C# types
  • Runtime validation: Request bodies validated against schema before reaching handlers
  • OpenAPI 3.1.0 compatibility: Native JSON Schema support in OpenAPI 3.1.0
  • Automatic documentation: Schemas flow to Gateway for unified OpenAPI documentation
  • External schema support: Override generated schemas with embedded resource files

Benefits

Benefit Description
Type safety Contract enforcement between clients and services
Early error detection Invalid requests rejected with 422 before handler execution
Documentation automation No manual schema maintenance required
Interoperability Standard 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

Property Type Default Description
ValidateRequest bool true Enable request body validation
ValidateResponse bool false Enable response body validation
RequestSchemaResource string? null Embedded resource path to external request schema
ResponseSchemaResource string? null Embedded resource path to external response schema
Summary string? null OpenAPI operation summary
Description string? null OpenAPI operation description
Tags string[]? null OpenAPI tags for grouping endpoints
Deprecated bool false Mark 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:

Header Value Description
Cache-Control public, max-age=60 Client-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:

  • Required property validation (required keyword)
  • Type validation (type keyword)
  • Nullable property handling (null in type union)
  • Nested object schemas

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:

  • Service: Billing
  • Type: CreateInvoiceRequest
  • Schema ID: Billing_CreateInvoiceRequest

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