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

427 lines
15 KiB
Markdown

# 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
```csharp
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
```csharp
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.
```csharp
[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.
```csharp
// 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.
```csharp
// Enable response validation
[ValidateSchema(ValidateResponse = true)]
```
#### External Schema Files
Override generated schemas with embedded resource files when you need custom schema definitions:
```csharp
[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:
```csharp
[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:
```csharp
[ValidateSchema(Tags = ["Billing", "Invoices"])]
public sealed class CreateInvoiceEndpoint : IStellaEndpoint<CreateInvoiceRequest, CreateInvoiceResponse>
```
#### Deprecated
Mark an endpoint as deprecated in OpenAPI documentation:
```csharp
[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
```http
GET /.well-known/openapi
```
Returns metadata about the OpenAPI document including available format URLs:
```json
{
"openapi_json": "/openapi.json",
"openapi_yaml": "/openapi.yaml",
"etag": "\"a1b2c3d4\"",
"generated_at": "2025-01-15T10:30:00.000Z"
}
```
### OpenAPI Document (JSON)
```http
GET /openapi.json
Accept: application/json
```
Returns the full OpenAPI 3.1.0 specification in JSON format. Supports ETag-based caching:
```http
GET /openapi.json
If-None-Match: "a1b2c3d4"
```
Returns `304 Not Modified` if the document hasn't changed.
### OpenAPI Document (YAML)
```http
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
```csharp
[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
```csharp
[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
```csharp
[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
```csharp
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:
```json
{
"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](openapi-aggregation.md) - How schemas become OpenAPI documentation
- [API Overview](../../api/overview.md) - General API conventions
- [Router Architecture](architecture.md) - Router system overview