- 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.
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 (
requiredkeyword) - Type validation (
typekeyword) - Nullable property handling (
nullin 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
- OpenAPI Aggregation - How schemas become OpenAPI documentation
- API Overview - General API conventions
- Router Architecture - Router system overview