# CVT (Contract Validator Toolkit)
> Consumer-based contract validation platform for OpenAPI v2/v3 specifications. Validates HTTP request/response interactions against registered schemas via gRPC server with SDKs for Node.js, Python, Go, and Java, plus a CLI for local validation.
CVT enables API contract testing by:
- Validating HTTP interactions against OpenAPI schemas (v2/v3)
- Detecting breaking changes between schema versions
- Tracking consumer dependencies via consumer registry
- Checking deployment safety with `can-i-deploy`
- Generating test fixtures from schemas
- Producer-side validation with framework middleware (Express, FastAPI, Gin, Spring, etc.)
- HTTP client adapters for transparent contract validation (Axios, Fetch, Requests, OkHttp)
## Quick Start
### Node.js
```typescript
import { ContractValidator } from "@sahina/cvt-sdk";
const validator = new ContractValidator("localhost:9550");
await validator.registerSchema("user-api", "./openapi.json");
const result = await validator.validate(
{ method: "GET", path: "/users/123", headers: {} },
{ statusCode: 200, body: { id: "123", name: "John" } }
);
if (!result.valid) {
console.log("Validation errors:", result.errors);
}
validator.close();
```
### Python
```python
from cvt_sdk import ContractValidator
validator = ContractValidator("localhost:9550")
validator.register_schema("user-api", "./openapi.json")
result = validator.validate(
request={"method": "GET", "path": "/users/123", "headers": {}},
response={"status_code": 200, "body": {"id": "123", "name": "John"}}
)
if not result["valid"]:
print("Validation errors:", result["errors"])
validator.close()
```
### Go (gRPC SDK)
```go
package main
import (
"context"
"fmt"
"log"
cvt "github.com/sahina/cvt/sdks/go/cvt"
)
func main() {
validator, err := cvt.NewValidator("localhost:9550")
if err != nil {
log.Fatal(err)
}
defer validator.Close()
ctx := context.Background()
err = validator.RegisterSchema(ctx, "user-api", "./openapi.json")
if err != nil {
log.Fatal(err)
}
result, err := validator.Validate(ctx,
cvt.ValidationRequest{Method: "GET", Path: "/users/123"},
cvt.ValidationResponse{StatusCode: 200, Body: map[string]any{"id": "123", "name": "John"}},
)
if err != nil {
log.Fatal(err)
}
if !result.Valid {
fmt.Println("Errors:", result.Errors)
}
}
```
### Go (Embedded Library)
```go
package main
import (
"fmt"
"github.com/sahina/cvt/pkg/cvt"
)
func main() {
validator := cvt.NewValidator()
err := validator.RegisterSchemaFromFile("user-api", "./openapi.json")
if err != nil {
panic(err)
}
result, err := validator.Validate("user-api", &cvt.Interaction{
Method: "GET",
Path: "/users/123",
StatusCode: 200,
ResponseBody: `{"id":"123","name":"John"}`,
})
if !result.Valid {
fmt.Println("Errors:", result.Errors)
}
}
```
### Java
```java
import io.github.sahina.sdk.ContractValidator;
import io.github.sahina.sdk.ValidationRequest;
import io.github.sahina.sdk.ValidationResponse;
import io.github.sahina.sdk.ValidationResult;
ContractValidator validator = new ContractValidator("localhost:9550");
validator.registerSchema("user-api", "./openapi.json");
ValidationResult result = validator.validate(
ValidationRequest.builder().method("GET").path("/users/123").build(),
ValidationResponse.builder().statusCode(200).body("{\"id\":\"123\",\"name\":\"John\"}").build()
);
if (!result.isValid()) {
System.out.println("Errors: " + result.getErrors());
}
validator.close();
```
### CLI
```bash
# Validate interaction
cvt validate --schema ./openapi.json --request req.json --response resp.json
# Compare schemas for breaking changes
cvt compare --old ./v1/openapi.json --new ./v2/openapi.json
# Generate test fixtures
cvt generate --schema ./openapi.json --method POST --path /users
# Check deployment safety
cvt can-i-deploy --schema my-api --version 2.0.0 --env prod --server localhost:9550
# Wait for server readiness
cvt wait --server localhost:9550 --timeout 120
# Register schema with server
cvt register-schema my-api ./openapi.yaml --version 2.0.0
```
## Node.js SDK
### Installation
```bash
npm install @sahina/cvt-sdk
```
### Initialization
```typescript
import { ContractValidator, ContractValidatorOptions, TLSOptions } from "@sahina/cvt-sdk";
// Simple connection
const validator = new ContractValidator("localhost:9550");
// With TLS and API key
const validator = new ContractValidator({
address: "localhost:9550",
tls: { enabled: true, rootCertPath: "./certs/ca.crt" },
apiKey: "cvt-dev-key-12345"
});
```
### Types
```typescript
interface ValidationRequest
{
method: string; // GET, POST, PUT, DELETE, etc.
path: string; // /users/123
headers?: Record;
body?: TBody;
}
interface ValidationResponse {
statusCode?: number;
status_code?: number; // Alternative naming
headers?: Record;
body?: TBody;
}
interface ValidationResult {
valid: boolean;
errors?: string[];
}
interface BreakingChange {
type: string; // ENDPOINT_REMOVED, REQUIRED_FIELD_ADDED, TYPE_CHANGED, etc.
path: string; // /users/{id}
method: string; // DELETE
description: string;
oldValue?: string;
newValue?: string;
}
interface EndpointUsage {
method: string;
path: string;
usedFields?: string[]; // ["email", "name"]
}
interface RegisterConsumerOptions {
consumerId: string;
consumerVersion: string;
schemaId: string;
schemaVersion: string;
environment: string; // dev, staging, prod
usedEndpoints?: EndpointUsage[];
}
interface ConsumerImpact {
consumerId: string;
consumerVersion: string;
currentSchemaVersion: string; // Version consumer was tested against
environment: string;
willBreak: boolean; // True if consumer will be affected
relevantChanges: BreakingChange[]; // Breaking changes affecting this consumer
}
interface CanIDeployResult {
safeToDeploy: boolean;
summary: string;
breakingChanges: BreakingChange[];
affectedConsumers: ConsumerImpact[];
}
```
### Methods
#### registerSchema(schemaId, schemaPath)
Registers an OpenAPI schema from file path or URL.
```typescript
await validator.registerSchema("user-api", "./openapi.json");
await validator.registerSchema("user-api", "https://api.example.com/openapi.json");
```
#### registerSchemaWithVersion(schemaId, schemaPath, version)
Registers schema with semantic version for comparison.
```typescript
await validator.registerSchemaWithVersion("user-api", "./openapi.json", "1.0.0");
```
#### validate(request, response)
Validates HTTP interaction against registered schema.
```typescript
const result = await validator.validate(
{ method: "POST", path: "/users", headers: {"Content-Type": "application/json"}, body: { name: "John" } },
{ statusCode: 201, body: { id: "1", name: "John" } }
);
// result: { valid: true, errors: [] }
```
#### compareSchemas(schemaId, oldVersion?, newVersion?)
Compares two schema versions for breaking changes.
```typescript
const result = await validator.compareSchemas("user-api", "1.0.0", "2.0.0");
if (!result.compatible) {
result.breakingChanges.forEach(change => {
console.log(`${change.type}: ${change.description}`);
});
}
```
#### generateFixture(method, path, options?)
Generates test fixture from schema.
```typescript
const fixture = await validator.generateFixture("POST", "/users", { useExamples: true });
// fixture: { request: { method, path, headers, body }, response: { statusCode, headers, body } }
```
#### generateResponse(method, path, options?)
Generates response fixture only.
```typescript
const response = await validator.generateResponse("GET", "/users/123");
// response: { statusCode: 200, headers: {}, body: { id: "123", name: "..." } }
```
#### generateRequestBody(method, path, options?)
Generates request body only.
```typescript
const body = await validator.generateRequestBody("POST", "/users");
// body: { name: "string", email: "string" }
```
#### listEndpoints()
Lists all endpoints in registered schema.
```typescript
const endpoints = await validator.listEndpoints();
// endpoints: [{ method: "GET", path: "/users", operationId: "getUsers", summary: "List users" }, ...]
```
#### registerConsumer(options)
Registers consumer dependency on schema.
```typescript
const consumer = await validator.registerConsumer({
consumerId: "order-service",
consumerVersion: "2.1.0",
schemaId: "user-api",
schemaVersion: "1.0.0",
environment: "prod",
usedEndpoints: [
{ method: "GET", path: "/users/{id}", usedFields: ["email", "name"] }
]
});
```
#### listConsumers(schemaId, environment?)
Lists consumers depending on a schema.
```typescript
const consumers = await validator.listConsumers("user-api", "prod");
```
#### deregisterConsumer(consumerId, schemaId, environment)
Removes consumer registration.
```typescript
await validator.deregisterConsumer("order-service", "user-api", "prod");
```
#### canIDeploy(schemaId, newVersion, environment)
Checks if schema can be safely deployed.
```typescript
const result = await validator.canIDeploy("user-api", "2.0.0", "prod");
if (!result.safeToDeploy) {
console.log(result.summary);
result.affectedConsumers.forEach(c => {
if (c.willBreak) console.log(`${c.consumerId} will break!`);
});
}
```
#### close()
Closes gRPC connection.
```typescript
validator.close();
```
#### buildConsumerFromInteractions(interactions, options)
Builds consumer registration from recorded interactions.
```typescript
const consumerData = validator.buildConsumerFromInteractions(
[
{ request: { method: "GET", path: "/users/123" }, response: { statusCode: 200, body: { id: "123" } } }
],
{ consumerId: "order-service", consumerVersion: "1.0.0", schemaId: "user-api", environment: "dev" }
);
```
#### registerConsumerFromInteractions(interactions, options)
Registers consumer directly from recorded interactions.
```typescript
await validator.registerConsumerFromInteractions(
[
{ request: { method: "GET", path: "/users/123" }, response: { statusCode: 200, body: { id: "123" } } }
],
{ consumerId: "order-service", consumerVersion: "1.0.0", schemaId: "user-api", schemaVersion: "1.0.0", environment: "dev" }
);
```
### Node.js Adapters (HTTP Client Integration)
Wrap HTTP clients for automatic contract validation. Captured interactions can be used for consumer auto-registration.
#### Axios Adapter
```typescript
import { createAxiosAdapter } from "@sahina/cvt-sdk/adapters";
import axios from "axios";
const api = axios.create({ baseURL: "http://localhost:3000" });
const adapter = createAxiosAdapter({
validator,
axios: api,
autoValidate: true, // validate every request (default: true)
includePaths: ["/users/**"], // only validate matching paths
excludePaths: ["/health"], // skip these paths
onValidationFailure: (result, req, res) => {
console.error("Contract violation:", result.errors);
},
});
// Use axios normally — validation happens automatically
const response = await api.get("/users/123");
// Get captured interactions for consumer registration
const interactions = adapter.getInteractions();
adapter.clearInteractions();
// Detach adapter when done
adapter.detach();
```
#### Fetch Adapter
```typescript
import { createValidatingFetch } from "@sahina/cvt-sdk/adapters";
const { fetch: validatedFetch, adapter } = createValidatingFetch({
validator,
baseURL: "http://localhost:3000",
autoValidate: true,
includePaths: ["/users/**"],
});
// Use like standard fetch
const response = await validatedFetch("/users/123");
// Get captured interactions
const interactions = adapter.getInteractions();
adapter.clearInteractions();
```
#### Mock Adapter
Generate schema-compliant mock responses for consumer testing without a real API.
```typescript
import { createMockFetch, createMockAdapter } from "@sahina/cvt-sdk/adapters";
// Simple mock fetch function
const mockFetch = createMockFetch(validator, { cache: true });
const response = await mockFetch("/users/123");
const data = await response.json();
// Full mock adapter with more control
const mock = createMockAdapter({
validator,
cache: true,
generateOptions: { useExamples: true },
includePaths: ["/users/**"],
});
const response = await mock.fetch("/users/123");
const interactions = mock.getInteractions();
mock.clearCache();
```
### Node.js Producer Validation (Server-Side)
Validate your API server's requests and responses against its OpenAPI schema.
**Validation Modes:**
- `strict` — Reject invalid requests with 400 errors; log response violations
- `warn` — Log all violations but allow requests to proceed
- `shadow` — Validate asynchronously, record metrics only (for production rollout)
#### Express Middleware
```typescript
import { createExpressMiddleware } from "@sahina/cvt-sdk/producer";
const middleware = createExpressMiddleware({
schemaId: "user-api",
validator,
mode: "strict",
validateRequest: true,
validateResponse: true,
includePaths: ["/api/**"],
excludePaths: ["/api/health"],
});
app.use(middleware);
```
#### Fastify Plugin
```typescript
import { fastifyProducerPlugin } from "@sahina/cvt-sdk/producer";
await app.register(fastifyProducerPlugin, {
schemaId: "user-api",
validator,
mode: "warn",
logPrefix: "cvt",
});
```
#### ProducerTestKit
Test your API responses against the schema without running middleware.
```typescript
import { ProducerTestKit } from "@sahina/cvt-sdk/producer";
const testKit = new ProducerTestKit({
schemaId: "user-api",
serverAddress: "localhost:9550",
});
// Validate a single response
const result = await testKit.validateResponse({
method: "GET",
path: "/users/123",
response: { statusCode: 200, body: { id: "123", name: "John" } },
});
expect(result.valid).toBe(true);
// Validate full interaction
const result = await testKit.validateInteraction({
request: { method: "POST", path: "/users", body: { name: "Jane" } },
response: { statusCode: 201, body: { id: "2", name: "Jane" } },
});
// Reusable endpoint tester
const userEndpoint = testKit.forEndpoint("GET", "/users/{id}");
const r1 = await userEndpoint.validateResponse(
{ statusCode: 200, body: { id: "1", name: "John" } },
{ pathValues: { id: "1" } }
);
testKit.close();
```
#### Metrics
```typescript
import { getMetrics, resetMetrics } from "@sahina/cvt-sdk/producer";
const metrics = getMetrics();
// {
// requestValidations, requestValidationsPassed, requestValidationsFailed,
// responseValidations, responseValidationsPassed, responseValidationsFailed,
// requestsRejected
// }
resetMetrics();
```
## Python SDK
### Installation
```bash
pip install cvt-sdk
```
### Initialization
```python
from cvt_sdk import ContractValidator, ContractValidatorOptions, TLSOptions
# Simple connection
validator = ContractValidator("localhost:9550")
# With TLS and API key
validator = ContractValidator(ContractValidatorOptions(
address="localhost:9550",
tls=TLSOptions(enabled=True, root_cert_path="./certs/ca.crt"),
api_key="cvt-dev-key-12345"
))
```
### Types
```python
from cvt_sdk import (
ValidationRequest, # TypedDict: method, path, headers, body
ValidationResponse, # TypedDict: status_code, headers, body
ValidationResult, # TypedDict: valid, errors
BreakingChange, # TypedDict: type, path, method, description
GenerateOptions, # dataclass: status_code, use_examples, content_type
EndpointUsage, # dataclass: method, path, used_fields
RegisterConsumerOptions,# dataclass: consumer_id, consumer_version, schema_id, etc.
CanIDeployResult, # TypedDict: safe_to_deploy, summary, breaking_changes, affected_consumers
)
```
### Methods
#### register_schema(schema_id, schema_path)
```python
validator.register_schema("user-api", "./openapi.json")
validator.register_schema("user-api", "https://api.example.com/openapi.json")
```
#### register_schema_with_version(schema_id, schema_path, version)
```python
validator.register_schema_with_version("user-api", "./openapi.json", "1.0.0")
```
#### validate(request, response)
```python
result = validator.validate(
request={"method": "POST", "path": "/users", "body": {"name": "John"}},
response={"status_code": 201, "body": {"id": "1", "name": "John"}}
)
# result: {"valid": True, "errors": []}
```
#### compare_schemas(schema_id, old_version="", new_version="")
```python
result = validator.compare_schemas("user-api", "1.0.0", "2.0.0")
if not result["compatible"]:
for change in result["breaking_changes"]:
print(f"{change['type']}: {change['description']}")
```
#### generate_fixture(method, path, options=None)
```python
from cvt_sdk import GenerateOptions
fixture = validator.generate_fixture("POST", "/users", GenerateOptions(use_examples=True))
# fixture: {"request": {...}, "response": {...}}
```
#### generate_response(method, path, options=None)
```python
response = validator.generate_response("GET", "/users/123")
# response: {"status_code": 200, "headers": {}, "body": {...}}
```
#### generate_request_body(method, path, options=None)
```python
body = validator.generate_request_body("POST", "/users")
# body: {"name": "string", "email": "string"}
```
#### list_endpoints()
```python
endpoints = validator.list_endpoints()
# endpoints: [{"method": "GET", "path": "/users", ...}, ...]
```
#### register_consumer(options)
```python
from cvt_sdk import RegisterConsumerOptions, EndpointUsage
consumer = validator.register_consumer(RegisterConsumerOptions(
consumer_id="order-service",
consumer_version="2.1.0",
schema_id="user-api",
schema_version="1.0.0",
environment="prod",
used_endpoints=[
EndpointUsage(method="GET", path="/users/{id}", used_fields=["email", "name"])
]
))
```
#### list_consumers(schema_id, environment=None)
```python
consumers = validator.list_consumers("user-api", "prod")
```
#### deregister_consumer(consumer_id, schema_id, environment)
```python
validator.deregister_consumer("order-service", "user-api", "prod")
```
#### can_i_deploy(schema_id, new_version, environment)
```python
result = validator.can_i_deploy("user-api", "2.0.0", "prod")
if not result["safe_to_deploy"]:
print(result["summary"])
```
#### close()
```python
validator.close()
```
#### build_consumer_from_interactions(interactions, options)
Builds consumer registration from recorded interactions.
```python
consumer_data = validator.build_consumer_from_interactions(
interactions=[
{"request": {"method": "GET", "path": "/users/123"}, "response": {"status_code": 200, "body": {"id": "123"}}}
],
options={"consumer_id": "order-service", "consumer_version": "1.0.0", "schema_id": "user-api", "environment": "dev"}
)
```
#### register_consumer_from_interactions(interactions, options)
Registers consumer directly from recorded interactions.
```python
validator.register_consumer_from_interactions(
interactions=[
{"request": {"method": "GET", "path": "/users/123"}, "response": {"status_code": 200, "body": {"id": "123"}}}
],
options={"consumer_id": "order-service", "consumer_version": "1.0.0", "schema_id": "user-api", "schema_version": "1.0.0", "environment": "dev"}
)
```
### Python Adapters (HTTP Client Integration)
#### Requests Session (ContractValidatingSession)
Drop-in replacement for `requests.Session` that validates every HTTP interaction.
```python
from cvt_sdk.adapters import ContractValidatingSession, create_validating_session
# Using factory function
session = create_validating_session(
validator,
auto_validate=True,
include_paths=["/users/**"],
exclude_paths=["/health"],
on_validation_failure=lambda result, req, res: print(f"Violation: {result}"),
)
# Use like a normal requests session
response = session.get("http://localhost:3000/users/123")
response = session.post("http://localhost:3000/users", json={"name": "Jane"})
# Get captured interactions for consumer registration
interactions = session.get_interactions()
session.clear_interactions()
# Validate a specific interaction
result = session.validate_interaction(interactions[0])
```
#### Mock Session
Generate schema-compliant mock responses for consumer testing without a real API.
```python
from cvt_sdk.adapters import MockSession, create_mock_session
mock = create_mock_session(
validator,
cache=True,
generate_options=GenerateOptions(use_examples=True),
include_paths=["/users/**"],
)
# HTTP methods work like requests.Session
response = mock.get("/users/123")
data = response.json()
response = mock.post("/users", json_body={"name": "Jane"})
# Get captured interactions
interactions = mock.get_interactions()
mock.clear_cache()
```
### Python Producer Validation (Server-Side)
**Validation Modes:**
- `ValidationMode.STRICT` — Reject invalid requests with error responses
- `ValidationMode.WARN` — Log violations but allow requests to proceed
- `ValidationMode.SHADOW` — Validate asynchronously, record metrics only
#### FastAPI / ASGI Middleware
```python
from cvt_sdk.producer import ProducerConfig, ValidationMode
from cvt_sdk.producer.adapters import ASGIMiddleware, create_fastapi_middleware
app = FastAPI()
config = ProducerConfig(
schema_id="user-api",
validator=validator,
mode=ValidationMode.STRICT,
validate_request=True,
validate_response=True,
include_paths=["/api/.*"],
exclude_paths=["/api/health"],
)
app.add_middleware(ASGIMiddleware, config=config)
# Or use factory function
MiddlewareClass = create_fastapi_middleware(config)
app.add_middleware(MiddlewareClass)
```
#### Flask / WSGI Middleware
```python
from cvt_sdk.producer import ProducerConfig, ValidationMode
from cvt_sdk.producer.adapters import WSGIMiddleware, create_flask_middleware
app = Flask(__name__)
config = ProducerConfig(
schema_id="user-api",
validator=validator,
mode=ValidationMode.WARN,
)
app.wsgi_app = WSGIMiddleware(app.wsgi_app, config=config)
# Or use factory function
middleware_factory = create_flask_middleware(config)
app.wsgi_app = middleware_factory(app.wsgi_app)
```
#### ProducerTestKit
```python
from cvt_sdk.producer import ProducerTestKit, ProducerTestConfig, MockResponse, MockRequest
test_kit = ProducerTestKit(ProducerTestConfig(
schema_id="user-api",
server_address="localhost:9550",
))
# Validate a response
result = test_kit.validate_response(
method="GET",
path="/users/123",
status_code=200,
body={"id": "123", "name": "John"},
)
assert result.valid
# Validate a full interaction
result = test_kit.validate_interaction(
request=MockRequest(method="POST", path="/users", body={"name": "Jane"}),
response=MockResponse(status_code=201, body={"id": "2", "name": "Jane"}),
)
# Reusable endpoint tester
user_endpoint = test_kit.for_endpoint("GET", "/users/{id}")
result = user_endpoint.validate_response(
status_code=200,
body={"id": "1", "name": "John"},
path_values={"id": "1"},
)
test_kit.close()
```
#### Metrics
```python
from cvt_sdk.producer import get_metrics, reset_metrics
metrics = get_metrics()
# {
# "request_validations", "request_validations_passed", "request_validations_failed",
# "response_validations", "response_validations_passed", "response_validations_failed",
# "requests_rejected"
# }
reset_metrics()
```
## Go gRPC SDK
Full gRPC client SDK with all RPC methods, TLS, and API key authentication.
### Installation
```bash
go get github.com/sahina/cvt/sdks/go
```
```go
import cvt "github.com/sahina/cvt/sdks/go/cvt"
```
### Initialization
```go
// Simple connection
validator, err := cvt.NewValidator("localhost:9550")
if err != nil {
log.Fatal(err)
}
defer validator.Close()
// With TLS and API key
validator, err := cvt.NewValidatorWithOptions(cvt.ValidatorOptions{
Address: "localhost:9550",
TLS: &cvt.TLSOptions{
Enabled: true,
RootCertPath: "./certs/ca.crt",
CertPath: "./certs/client.crt", // For mTLS
KeyPath: "./certs/client.key", // For mTLS
},
APIKey: "cvt-dev-key-12345",
})
```
### Types
```go
type ValidatorOptions struct {
Address string
TLS *TLSOptions
APIKey string
}
type TLSOptions struct {
Enabled bool
RootCertPath string
CertPath string // Client cert for mTLS
KeyPath string // Client key for mTLS
}
type ValidationRequest struct {
Method string
Path string
Headers map[string]string
Body any
}
type ValidationResponse struct {
StatusCode int
Headers map[string]string
Body any
}
type ValidationResult struct {
Valid bool
Errors []string
}
type CompareResult struct {
Compatible bool
BreakingChanges []BreakingChange
}
type BreakingChange struct {
Type string
Path string
Method string
Description string
OldValue string
NewValue string
}
type GenerateOptions struct {
StatusCode int
UseExamples bool
ContentType string
}
type GeneratedFixture struct {
Request *GeneratedRequest
Response *GeneratedResponse
}
type GeneratedRequest struct {
Method string
Path string
Headers map[string]string
Body any
}
type GeneratedResponse struct {
StatusCode int
Headers map[string]string
Body any
}
type EndpointInfo struct {
Method string
Path string
OperationID string
Summary string
}
type EndpointUsage struct {
Method string
Path string
UsedFields []string
}
type RegisterConsumerOptions struct {
ConsumerID string
ConsumerVersion string
SchemaID string
SchemaVersion string
Environment string
UsedEndpoints []EndpointUsage
}
type ConsumerInfo struct {
ConsumerID string
ConsumerVersion string
SchemaID string
SchemaVersion string
Environment string
RegisteredAt int64
LastValidatedAt int64
UsedEndpoints []EndpointUsage
}
type ConsumerImpact struct {
ConsumerID string
ConsumerVersion string
CurrentSchemaVersion string
Environment string
WillBreak bool
RelevantChanges []BreakingChange
}
type CanIDeployResult struct {
SafeToDeploy bool
Summary string
BreakingChanges []BreakingChange
AffectedConsumers []ConsumerImpact
}
type CapturedInteraction struct {
Request ValidationRequest
Response ValidationResponse
ValidationResult *ValidationResult
Timestamp time.Time
}
type AutoRegisterConfig struct {
ConsumerID string
ConsumerVersion string
Environment string
SchemaVersion string
SchemaID string // optional, auto-extracted if empty
}
```
### Methods
All methods take a `context.Context` as the first parameter.
#### RegisterSchema(ctx, schemaID, schemaPath) error
```go
err := validator.RegisterSchema(ctx, "user-api", "./openapi.json")
err := validator.RegisterSchema(ctx, "user-api", "https://api.example.com/openapi.json")
```
#### RegisterSchemaWithVersion(ctx, schemaID, schemaPath, version) error
```go
err := validator.RegisterSchemaWithVersion(ctx, "user-api", "./openapi.json", "1.0.0")
```
#### Validate(ctx, request, response) (*ValidationResult, error)
```go
result, err := validator.Validate(ctx,
cvt.ValidationRequest{
Method: "POST",
Path: "/users",
Headers: map[string]string{"Content-Type": "application/json"},
Body: map[string]any{"name": "John"},
},
cvt.ValidationResponse{
StatusCode: 201,
Body: map[string]any{"id": "1", "name": "John"},
},
)
if !result.Valid {
fmt.Println("Errors:", result.Errors)
}
```
#### CompareSchemas(ctx, schemaID, oldVersion, newVersion) (*CompareResult, error)
```go
result, err := validator.CompareSchemas(ctx, "user-api", "1.0.0", "2.0.0")
if !result.Compatible {
for _, change := range result.BreakingChanges {
fmt.Printf("%s: %s\n", change.Type, change.Description)
}
}
```
#### GenerateFixture(ctx, method, path, opts) (*GeneratedFixture, error)
```go
fixture, err := validator.GenerateFixture(ctx, "POST", "/users",
&cvt.GenerateOptions{UseExamples: true})
fmt.Println("Request body:", fixture.Request.Body)
fmt.Println("Response:", fixture.Response.StatusCode, fixture.Response.Body)
```
#### GenerateResponse(ctx, method, path, opts) (*GeneratedResponse, error)
```go
response, err := validator.GenerateResponse(ctx, "GET", "/users/123", nil)
fmt.Println("Status:", response.StatusCode, "Body:", response.Body)
```
#### GenerateRequestBody(ctx, method, path, opts) (any, error)
```go
body, err := validator.GenerateRequestBody(ctx, "POST", "/users", nil)
```
#### ListEndpoints(ctx) ([]EndpointInfo, error)
```go
endpoints, err := validator.ListEndpoints(ctx)
for _, ep := range endpoints {
fmt.Printf("%s %s - %s\n", ep.Method, ep.Path, ep.Summary)
}
```
#### RegisterConsumer(ctx, opts) (*ConsumerInfo, error)
```go
consumer, err := validator.RegisterConsumer(ctx, cvt.RegisterConsumerOptions{
ConsumerID: "order-service",
ConsumerVersion: "2.1.0",
SchemaID: "user-api",
SchemaVersion: "1.0.0",
Environment: "prod",
UsedEndpoints: []cvt.EndpointUsage{
{Method: "GET", Path: "/users/{id}", UsedFields: []string{"email", "name"}},
},
})
```
#### ListConsumers(ctx, schemaID, environment) ([]ConsumerInfo, error)
```go
consumers, err := validator.ListConsumers(ctx, "user-api", "prod")
```
#### DeregisterConsumer(ctx, consumerID, schemaID, environment) error
```go
err := validator.DeregisterConsumer(ctx, "order-service", "user-api", "prod")
```
#### CanIDeploy(ctx, schemaID, newVersion, environment) (*CanIDeployResult, error)
```go
result, err := validator.CanIDeploy(ctx, "user-api", "2.0.0", "prod")
if !result.SafeToDeploy {
fmt.Println(result.Summary)
for _, c := range result.AffectedConsumers {
if c.WillBreak {
fmt.Printf("%s will break!\n", c.ConsumerID)
}
}
}
```
#### BuildConsumerFromInteractions(ctx, interactions, config) (*RegisterConsumerOptions, error)
```go
opts, err := validator.BuildConsumerFromInteractions(ctx, interactions,
cvt.AutoRegisterConfig{
ConsumerID: "order-service",
ConsumerVersion: "1.0.0",
Environment: "dev",
SchemaVersion: "1.0.0",
})
```
#### RegisterConsumerFromInteractions(ctx, interactions, config) (*ConsumerInfo, error)
```go
consumer, err := validator.RegisterConsumerFromInteractions(ctx, interactions,
cvt.AutoRegisterConfig{
ConsumerID: "order-service",
ConsumerVersion: "1.0.0",
Environment: "dev",
SchemaVersion: "1.0.0",
SchemaID: "user-api",
})
```
#### Close() error
```go
err := validator.Close()
```
### Go Adapters (HTTP Client Integration)
#### ValidatingRoundTripper
Wraps `http.RoundTripper` for transparent client-side validation.
```go
import "github.com/sahina/cvt/sdks/go/cvt/adapters"
rt := adapters.NewValidatingRoundTripper(adapters.RoundTripperConfig{
Validator: validator,
Transport: http.DefaultTransport,
AutoValidate: true,
IncludePaths: []adapters.PathFilter{"/users/**"},
ExcludePaths: []adapters.PathFilter{"/health"},
OnValidationFailure: func(result *cvt.ValidationResult, req *http.Request, resp *http.Response) error {
log.Printf("Contract violation on %s %s: %v", req.Method, req.URL.Path, result.Errors)
return nil // return error to fail the request
},
})
client := &http.Client{Transport: rt}
resp, err := client.Get("http://localhost:3000/users/123")
// Get captured interactions for consumer registration
interactions := rt.GetInteractions()
rt.ClearInteractions()
```
#### ValidatingMiddleware (Server-Side)
```go
middleware := adapters.NewValidatingMiddleware(adapters.MiddlewareConfig{
Validator: validator,
AutoValidate: true,
IncludePaths: []adapters.PathFilter{"/api/**"},
})
mux := http.NewServeMux()
mux.Handle("/api/users", middleware.Handler(userHandler))
// or
mux.Handle("/api/users", middleware.HandlerFunc(userHandlerFunc))
interactions := middleware.GetInteractions()
```
#### Mock Client
Generate schema-compliant mock responses for testing.
```go
import "github.com/sahina/cvt/sdks/go/cvt/adapters"
// Simple mock client
mockClient := adapters.NewMockClient(validator,
adapters.WithCache(),
adapters.WithRequestValidation(),
adapters.WithGenerateOptions(&cvt.GenerateOptions{UseExamples: true}),
)
resp, err := mockClient.Get("http://api.example.com/users/123")
// Full control with Mock wrapper
mock := adapters.NewMock(validator, adapters.WithCache())
client := mock.Client()
resp, err := client.Get("http://api.example.com/users/123")
interactions := mock.GetInteractions()
mock.ClearCache()
```
### Go Producer Validation (Server-Side)
```go
import (
"github.com/sahina/cvt/sdks/go/cvt/producer"
producerAdapters "github.com/sahina/cvt/sdks/go/cvt/producer/adapters"
)
```
**Validation Modes:**
- `producer.ModeStrict` — Reject invalid requests
- `producer.ModeWarn` — Log violations, allow requests
- `producer.ModeShadow` — Async validation, metrics only
#### net/http Middleware
```go
middleware := producerAdapters.NetHTTPMiddleware(producer.Config{
SchemaID: "user-api",
Validator: validator,
Mode: producer.ModeStrict,
ValidateRequest: true,
ValidateResponse: true,
IncludePaths: []producer.PathFilter{"/api/.*"},
ExcludePaths: []producer.PathFilter{"/api/health"},
})
mux := http.NewServeMux()
handler := middleware(mux)
http.ListenAndServe(":8080", handler)
```
#### Chi Middleware
```go
r := chi.NewRouter()
r.Use(producerAdapters.ChiMiddleware(producer.Config{
SchemaID: "user-api",
Validator: validator,
Mode: producer.ModeWarn,
}))
```
#### Gin Middleware
```go
r := gin.Default()
r.Use(producerAdapters.GinMiddleware(producer.Config{
SchemaID: "user-api",
Validator: validator,
Mode: producer.ModeShadow,
}))
```
#### ProducerTestKit
```go
import "github.com/sahina/cvt/sdks/go/cvt/producer"
testKit, err := producer.NewProducerTestKit(producer.TestConfig{
SchemaID: "user-api",
ServerAddress: "localhost:9550",
})
defer testKit.Close()
// Validate a response
result, err := testKit.ValidateResponse(ctx, producer.ValidateResponseParams{
Method: "GET",
Path: "/users/123",
Response: producer.TestResponseData{StatusCode: 200, Body: map[string]any{"id": "123", "name": "John"}},
})
// Validate full interaction
result, err := testKit.ValidateInteraction(ctx,
producer.TestRequestContext{Method: "POST", Path: "/users", Body: map[string]any{"name": "Jane"}},
producer.TestResponseData{StatusCode: 201, Body: map[string]any{"id": "2", "name": "Jane"}},
)
// Reusable endpoint tester
userEndpoint := testKit.ForEndpoint("GET", "/users/{id}")
result, err := userEndpoint.ValidateResponse(ctx,
producer.TestResponseData{StatusCode: 200, Body: map[string]any{"id": "1"}},
map[string]string{"id": "1"},
)
```
#### Metrics
```go
metrics := producer.GetMetrics()
snapshot := metrics.Snapshot()
fmt.Println("Request validations:", snapshot.RequestValidations)
fmt.Println("Failed:", snapshot.RequestValidationsFailed)
producer.ResetMetrics()
```
## Go Embedded Library
The embedded library allows local validation without a gRPC server.
### Installation
```go
import "github.com/sahina/cvt/pkg/cvt"
```
### Types
```go
type Interaction struct {
Method string `json:"method"` // GET, POST, etc.
Path string `json:"path"` // /users/123
Headers map[string]string `json:"headers,omitempty"`
Body string `json:"body,omitempty"` // JSON string
StatusCode int `json:"status_code"`
ResponseHeaders map[string]string `json:"response_headers,omitempty"`
ResponseBody string `json:"response_body"` // JSON string
}
type ValidationResult struct {
Valid bool `json:"valid"`
Errors []string `json:"errors,omitempty"`
}
```
### Methods
#### NewValidator()
```go
validator := cvt.NewValidator()
```
#### RegisterSchema(schemaID string, content []byte) error
```go
content, _ := os.ReadFile("./openapi.json")
err := validator.RegisterSchema("user-api", content)
```
#### RegisterSchemaFromFile(schemaID, filePath string) error
```go
err := validator.RegisterSchemaFromFile("user-api", "./openapi.json")
```
#### Validate(schemaID string, interaction *Interaction) (*ValidationResult, error)
```go
result, err := validator.Validate("user-api", &cvt.Interaction{
Method: "GET",
Path: "/users/123",
Headers: map[string]string{"Accept": "application/json"},
StatusCode: 200,
ResponseBody: `{"id":"123","name":"John","email":"john@example.com"}`,
})
if !result.Valid {
fmt.Println("Errors:", result.Errors)
}
```
#### ValidateWithSchema(schemaContent []byte, interaction *Interaction) (*ValidationResult, error)
```go
schemaContent, _ := os.ReadFile("./openapi.json")
result, err := validator.ValidateWithSchema(schemaContent, interaction)
```
#### ListSchemas() []string
```go
schemas := validator.ListSchemas()
```
#### RemoveSchema(schemaID string)
```go
validator.RemoveSchema("user-api")
```
#### GetSchema(schemaID string) (*openapi3.T, bool)
```go
doc, found := validator.GetSchema("user-api")
```
## Java SDK
### Installation
Maven:
```xml
io.github.sahina
cvt-sdk
0.1.0
```
Gradle:
```gradle
dependencies {
implementation 'io.github.sahina:cvt-sdk:0.1.0'
}
```
### Initialization
```java
import io.github.sahina.sdk.ContractValidator;
// Simple connection
ContractValidator validator = new ContractValidator("localhost:9550");
// With TLS and API key
ContractValidator validator = ContractValidator.builder()
.address("localhost:9550")
.tlsEnabled(true)
.rootCertPath("./certs/ca.crt")
.apiKey("cvt-dev-key-12345")
.build();
```
### Types
```java
import io.github.sahina.sdk.ValidationRequest;
import io.github.sahina.sdk.ValidationResponse;
import io.github.sahina.sdk.ValidationResult;
import io.github.sahina.sdk.CompareResult;
import io.github.sahina.sdk.BreakingChange;
import io.github.sahina.sdk.GenerateOptions;
import io.github.sahina.sdk.GeneratedFixture;
import io.github.sahina.sdk.GeneratedResponse;
import io.github.sahina.sdk.EndpointInfo;
import io.github.sahina.sdk.EndpointUsage;
import io.github.sahina.sdk.RegisterConsumerOptions;
import io.github.sahina.sdk.ConsumerInfo;
import io.github.sahina.sdk.CanIDeployResult;
import io.github.sahina.sdk.ConsumerImpact;
// Validation types
ValidationRequest request = ValidationRequest.builder()
.method("GET")
.path("/users/123")
.headers(Map.of("Content-Type", "application/json"))
.build();
ValidationResponse response = ValidationResponse.builder()
.statusCode(200)
.body("{\"id\": \"123\", \"name\": \"John\"}")
.build();
ValidationResult result = validator.validate(request, response);
// result.isValid(), result.getErrors()
// Breaking change types
BreakingChange change; // type, path, method, description, oldValue, newValue
CompareResult compareResult; // compatible, breakingChanges
// Consumer registry types
RegisterConsumerOptions options; // consumerId, consumerVersion, schemaId, schemaVersion, environment, usedEndpoints
ConsumerInfo info; // consumerId, consumerVersion, schemaId, schemaVersion, environment, registeredAt, lastValidatedAt, usedEndpoints
CanIDeployResult deployResult; // safeToDeploy, summary, breakingChanges, affectedConsumers
```
### Methods
#### registerSchema(schemaId, schemaPath)
```java
validator.registerSchema("user-api", "./openapi.json");
validator.registerSchema("user-api", "https://api.example.com/openapi.json");
```
#### registerSchemaWithVersion(schemaId, schemaPath, version)
```java
validator.registerSchemaWithVersion("user-api", "./openapi.json", "1.0.0");
```
#### validate(request, response)
```java
ValidationResult result = validator.validate(
ValidationRequest.builder()
.method("POST")
.path("/users")
.body("{\"name\": \"John\"}")
.build(),
ValidationResponse.builder()
.statusCode(201)
.body("{\"id\": \"1\", \"name\": \"John\"}")
.build()
);
// result: ValidationResult(valid=true, errors=[])
```
#### compareSchemas(schemaId, oldVersion, newVersion)
```java
CompareResult result = validator.compareSchemas("user-api", "1.0.0", "2.0.0");
if (!result.isCompatible()) {
for (BreakingChange change : result.getBreakingChanges()) {
System.out.println(change.getType() + ": " + change.getDescription());
}
}
```
#### generateFixture(method, path, options)
```java
GeneratedFixture fixture = validator.generateFixture("POST", "/users",
GenerateOptions.builder().useExamples(true).build());
// fixture: GeneratedFixture(request, response)
```
#### generateResponse(method, path, options)
```java
GeneratedResponse response = validator.generateResponse("GET", "/users/123", null);
// response: GeneratedResponse(statusCode, headers, body)
```
#### generateRequestBody(method, path, options)
```java
Object body = validator.generateRequestBody("POST", "/users", null);
// body: Map or List depending on schema
```
#### listEndpoints()
```java
List endpoints = validator.listEndpoints();
for (EndpointInfo ep : endpoints) {
System.out.println(ep.getMethod() + " " + ep.getPath());
}
```
#### registerConsumer(options)
```java
ConsumerInfo consumer = validator.registerConsumer(RegisterConsumerOptions.builder()
.consumerId("order-service")
.consumerVersion("2.1.0")
.schemaId("user-api")
.schemaVersion("1.0.0")
.environment("prod")
.usedEndpoints(List.of(
new EndpointUsage("GET", "/users/{id}", List.of("email", "name"))
))
.build());
```
#### listConsumers(schemaId, environment)
```java
List consumers = validator.listConsumers("user-api", "prod");
```
#### deregisterConsumer(consumerId, schemaId, environment)
```java
validator.deregisterConsumer("order-service", "user-api", "prod");
```
#### canIDeploy(schemaId, newVersion, environment)
```java
CanIDeployResult result = validator.canIDeploy("user-api", "2.0.0", "prod");
if (!result.isSafeToDeploy()) {
System.out.println(result.getSummary());
for (ConsumerImpact c : result.getAffectedConsumers()) {
if (c.isWillBreak()) {
System.out.println(c.getConsumerId() + " will break!");
}
}
}
```
#### close()
```java
validator.close();
// Or use try-with-resources since ContractValidator implements AutoCloseable
try (ContractValidator validator = new ContractValidator("localhost:9550")) {
// use validator
}
```
#### buildConsumerFromInteractions(interactions, options)
Builds consumer registration from recorded interactions.
```java
import io.github.sahina.sdk.adapters.CapturedInteraction;
import io.github.sahina.sdk.AutoRegisterConfig;
AutoRegisterUtils.BuildResult consumerData = validator.buildConsumerFromInteractions(
List.of(new CapturedInteraction("GET", "/users/123", Map.of(), null, 200, Map.of(),
"{\"id\":\"123\"}", null)),
AutoRegisterConfig.builder()
.consumerId("order-service")
.consumerVersion("1.0.0")
.schemaId("user-api")
.environment("dev")
.build()
);
```
#### registerConsumerFromInteractions(interactions, options)
Registers consumer directly from recorded interactions.
```java
ConsumerInfo consumer = validator.registerConsumerFromInteractions(
List.of(new CapturedInteraction("GET", "/users/123", Map.of(), null, 200, Map.of(),
"{\"id\":\"123\"}", null)),
AutoRegisterConfig.builder()
.consumerId("order-service")
.consumerVersion("1.0.0")
.schemaId("user-api")
.schemaVersion("1.0.0")
.environment("dev")
.build()
);
```
### Java Adapters (HTTP Client Integration)
#### OkHttp Adapter
Intercepts OkHttp requests for automatic contract validation.
```java
import io.github.sahina.sdk.adapters.OkHttpContractAdapter;
import io.github.sahina.sdk.adapters.AdapterConfig;
import io.github.sahina.sdk.adapters.CapturedInteraction;
OkHttpContractAdapter adapter = new OkHttpContractAdapter(validator)
.withConfig(AdapterConfig.builder()
.autoValidate(true)
.includePath("/users/.*")
.excludePath("/health")
.captureInteractions(true)
.build());
OkHttpClient client = adapter.attach(new OkHttpClient.Builder()).build();
// Use client normally — validation happens automatically
Request request = new Request.Builder().url("http://localhost:3000/users/123").build();
Response response = client.newCall(request).execute();
// Get captured interactions for consumer registration
List interactions = adapter.getInteractions();
List invalid = adapter.getInvalidInteractions();
adapter.clearInteractions();
```
#### Mock Interceptor
Generate schema-compliant mock responses for testing.
```java
import io.github.sahina.sdk.adapters.MockInterceptor;
import io.github.sahina.sdk.adapters.MockConfig;
// Simple mock client
OkHttpClient mockClient = MockInterceptor.createMockClient(validator);
// With configuration
OkHttpClient mockClient = MockInterceptor.createMockClient(validator,
MockConfig.builder()
.cache(true)
.generateOptions(GenerateOptions.builder().useExamples(true).build())
.includePath("/users/.*")
.build());
Request request = new Request.Builder().url("http://api.example.com/users/123").build();
Response response = mockClient.newCall(request).execute();
String body = response.body().string();
// Full control
MockInterceptor mock = new MockInterceptor(validator)
.withConfig(MockConfig.builder().cache(true).build());
List interactions = mock.getInteractions();
mock.clearCache();
```
### Java Producer Validation (Server-Side)
```java
import io.github.sahina.sdk.producer.*;
import io.github.sahina.sdk.producer.adapters.*;
```
**Validation Modes:**
- `ValidationMode.STRICT` — Reject invalid requests with 400/422 errors
- `ValidationMode.WARN` — Log violations but allow requests to proceed
- `ValidationMode.SHADOW` — Validate asynchronously, record metrics only
#### Spring Interceptor
Requires Spring 6+ / Jakarta Servlet API. Use with `ContentCachingRequestWrapper` and `ContentCachingResponseWrapper`.
```java
import io.github.sahina.sdk.producer.adapters.SpringInterceptor;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
ProducerConfig config = ProducerConfig.builder()
.schemaId("user-api")
.validator(myValidator)
.mode(ValidationMode.STRICT)
.validateRequest(true)
.validateResponse(true)
.includePath("/api/.*")
.excludePath("/api/health")
.build();
registry.addInterceptor(new SpringInterceptor(config))
.addPathPatterns("/api/**");
}
}
```
#### Servlet Filter
Works with any Jakarta Servlet container (Tomcat 10+, Jetty 11+).
```java
import io.github.sahina.sdk.producer.adapters.ServletFilter;
ProducerConfig config = ProducerConfig.builder()
.schemaId("user-api")
.validator(myValidator)
.mode(ValidationMode.WARN)
.build();
// Register as a filter
FilterRegistrationBean filterRegistration = new FilterRegistrationBean<>();
filterRegistration.setFilter(new ServletFilter(config));
filterRegistration.addUrlPatterns("/api/*");
```
#### ProducerTestKit
```java
import io.github.sahina.sdk.producer.*;
ProducerTestKit testKit = ProducerTestKit.builder()
.schemaId("user-api")
.serverAddress("localhost:9550")
.build();
// Validate a response
TestValidationResult result = testKit.validateResponse("GET", "/users/123",
TestResponseData.builder()
.statusCode(200)
.body(Map.of("id", "123", "name", "John"))
.build());
assertTrue(result.isValid());
// Validate full interaction
TestValidationResult result = testKit.validateInteraction(
TestRequestContext.builder().method("POST").path("/users").body(Map.of("name", "Jane")).build(),
TestResponseData.builder().statusCode(201).body(Map.of("id", "2", "name", "Jane")).build()
);
// Reusable endpoint tester
ProducerTestKit.EndpointTester userEndpoint = testKit.forEndpoint("GET", "/users/{id}");
TestValidationResult r = userEndpoint.validateResponse(
TestResponseData.builder().statusCode(200).body(Map.of("id", "1")).build(),
Map.of("id", "1")
);
testKit.close();
```
#### Metrics
```java
Producer.Metrics metrics = Producer.getMetrics();
System.out.println("Request validations: " + metrics.getRequestValidations());
System.out.println("Failed: " + metrics.getRequestValidationsFailed());
Producer.resetMetrics();
```
## CLI Reference
### validate
Validate HTTP interaction against OpenAPI schema.
```bash
# From separate request/response files
cvt validate --schema ./openapi.json --request req.json --response resp.json
# From combined interaction file
cvt validate --schema ./openapi.json --interaction interaction.json
# Inline with flags
cvt validate --schema ./openapi.json \
--method GET --path /users/123 \
--status-code 200 --response-body '{"id":"123","name":"John"}'
# Inline with request body
cvt validate --schema ./openapi.json \
--method POST --path /users \
--request-body '{"name":"John"}' \
--status-code 201 --response-body '{"id":"1","name":"John"}'
# JSON output
cvt validate --schema ./openapi.json --interaction interaction.json --json
# Schema from URL
cvt validate --schema https://petstore3.swagger.io/api/v3/openapi.json --interaction interaction.json
```
Flags:
- `--schema, -s` (required) — Path or URL to OpenAPI schema
- `--interaction, -i` — Path to interaction JSON file
- `--request` — Path to request JSON file
- `--response` — Path to response JSON file
- `--method, -m` — HTTP method (GET, POST, etc.)
- `--path, -p` — Request path (e.g., /users/123)
- `--request-body` — Request body as JSON string
- `--status-code` — Response status code (default: 200)
- `--response-body` — Response body as JSON string
- `--json` — Output result as JSON
### compare
Compare schemas for breaking changes.
```bash
cvt compare --old ./v1/openapi.json --new ./v2/openapi.json
cvt compare --old ./v1/openapi.json --new ./v2/openapi.json --json
cvt compare --old ./local.json --new https://api.example.com/openapi.json
```
Flags:
- `--old` (required) — Path or URL to old schema
- `--new` (required) — Path or URL to new schema
- `--json` — Output result as JSON
Breaking change types detected:
- ENDPOINT_REMOVED: Endpoint path+method was removed
- REQUIRED_FIELD_ADDED: Required field added to request body
- TYPE_CHANGED: Field type changed incompatibly
- REQUIRED_PARAMETER_ADDED: Required query/path/header param added
- RESPONSE_SCHEMA_CHANGED: Response schema changed incompatibly
- ENUM_VALUE_REMOVED: Enum value was removed
### generate
Generate test fixtures from schema.
```bash
# List all endpoints
cvt generate --schema ./openapi.json --list
# Generate fixture (request + response)
cvt generate --schema ./openapi.json --method POST --path /users
# Generate response only
cvt generate --schema ./openapi.json --method GET --path /users/{id} --output-type response
# Generate request only
cvt generate --schema ./openapi.json --method POST --path /users --output-type request
# Use schema examples
cvt generate --schema ./openapi.json --method GET --path /users/{id} --use-examples
# Save to file
cvt generate --schema ./openapi.json --method POST --path /users -o fixture.json
# Specify status code and content type
cvt generate --schema ./openapi.json --method GET --path /users/{id} --status-code 404
cvt generate --schema ./openapi.json --method POST --path /users --content-type application/json
```
Flags:
- `--schema, -s` (required) — Path or URL to OpenAPI schema
- `--method, -m` — HTTP method
- `--path, -p` — API path (e.g., /users/{id})
- `--status-code` — Response status code (default: first successful)
- `--use-examples` — Use schema examples when available (default: true)
- `--content-type` — Content type (default: application/json)
- `--output, -o` — Output file path (default: stdout)
- `--output-type, -t` — Output type: fixture, request, response (default: fixture)
- `--list, -l` — List available endpoints and exit
### serve
Start gRPC server.
```bash
cvt serve --port 9550
cvt serve --port 9550 --metrics-port 9551
cvt serve --port 9550 --tls --cert server.crt --key server.key
cvt serve --port 9550 --api-key-auth
```
Flags:
- `--port, -p` — gRPC server port (default: 9550)
- `--metrics-port` — Metrics server port (default: 9551)
- `--tls` — Enable TLS
- `--cert` — TLS certificate file
- `--key` — TLS private key file
- `--api-key-auth` — Enable API key authentication
### can-i-deploy
Check deployment safety.
```bash
cvt can-i-deploy --schema my-api --version 2.0.0 --env prod
cvt can-i-deploy --schema my-api --version 2.0.0 --env prod --server localhost:9550
cvt can-i-deploy --schema my-api --version 2.0.0 --env prod --json
cvt can-i-deploy --schema my-api --version 2.0.0 --env prod --timeout 60
```
Flags:
- `--schema` (required) — Schema ID to check
- `--version` (required) — New version to deploy
- `--env` (required) — Target environment: dev, staging, prod
- `--server` — CVT server address (default: localhost:9550)
- `--json` — Output result as JSON
- `--timeout` — Connection timeout in seconds (default: 30)
### wait
Wait for CVT server to be ready.
```bash
cvt wait # Default: 60s timeout, 2s interval
cvt wait --server localhost:9550 # Specific server address
cvt wait --timeout 120 --interval 1 # Custom timeout and polling interval
cvt wait -S localhost:9550 -t 120 -i 1 # Short flags
cvt wait -q # Quiet mode for CI/CD
cvt wait --json # JSON output for scripting
```
Flags:
- `--server, -S` — CVT server address (default: localhost:9550)
- `--timeout, -t` — Maximum time to wait in seconds (default: 60)
- `--interval, -i` — Polling interval in seconds (default: 2)
- `--quiet, -q` — Suppress output except errors
- `--json, -j` — Output result as JSON
JSON output:
```json
{
"status": "ready",
"server": "localhost:9550",
"attempts": 3
}
```
### register-schema
Register an OpenAPI schema with the CVT server.
```bash
cvt register-schema my-api ./openapi.yaml # Basic registration
cvt register-schema my-api https://api.example.com/openapi.json # Register from URL
cvt register-schema my-api ./openapi.yaml --version 2.0.0 # With specific version
cvt register-schema my-api ./openapi.yaml -v 2.0.0 # Short flag
cvt register-schema my-api ./openapi.yaml --check-compatibility # Check for breaking changes
cvt register-schema my-api ./openapi.yaml --check-compatibility --fail-on-breaking # Fail CI on breaking
cvt register-schema my-api ./openapi.yaml --json # JSON output for scripting
cvt register-schema my-api ./openapi.yaml --owner "Jane Doe" --team "Platform" # With ownership
cvt register-schema my-api ./openapi.yaml -q # Quiet mode
```
Positional arguments:
- `` (required) — Unique identifier for the schema
- `` (required) — Path or URL to OpenAPI schema file
Flags:
- `--server, -S` — CVT server address (default: localhost:9550)
- `--version, -v` — Schema version (optional, extracted from spec if not provided)
- `--check-compatibility` — Check for breaking changes against previous version
- `--fail-on-breaking` — Exit with error if breaking changes detected (use with --check-compatibility)
- `--json, -j` — Output result as JSON
- `--quiet, -q` — Suppress output except errors
- `--timeout, -t` — Connection timeout in seconds (default: 30)
- `--owner` — Schema owner name
- `--team` — Owning team name
JSON output:
```json
{
"success": true,
"message": "Schema registered successfully",
"schema_id": "my-api",
"version": "2.0.0",
"breaking_changes": []
}
```
### version
Show version information.
```bash
cvt version
```
## gRPC API
### Service: ContractValidator
```protobuf
service ContractValidator {
// Schema Management
rpc RegisterSchema(RegisterSchemaRequest) returns (RegisterSchemaResponse);
rpc GetSchema(GetSchemaRequest) returns (GetSchemaResponse);
rpc ListSchemas(ListSchemasRequest) returns (ListSchemasResponse);
// Validation
rpc ValidateInteraction(InteractionRequest) returns (ValidationResult);
rpc ValidateProducerResponse(ValidateProducerRequest) returns (ValidationResult);
// Schema Comparison
rpc CompareSchemas(CompareSchemasRequest) returns (CompareSchemasResponse);
// Fixtures
rpc GenerateFixture(GenerateFixtureRequest) returns (GenerateFixtureResponse);
rpc ListEndpoints(ListEndpointsRequest) returns (ListEndpointsResponse);
// Consumer Registry
rpc RegisterConsumer(RegisterConsumerRequest) returns (RegisterConsumerResponse);
rpc ListConsumers(ListConsumersRequest) returns (ListConsumersResponse);
rpc DeregisterConsumer(DeregisterConsumerRequest) returns (DeregisterConsumerResponse);
// Deployment Safety
rpc CanIDeploy(CanIDeployRequest) returns (CanIDeployResponse);
}
```
### Message Types
```protobuf
// ===== Schema Management =====
message SchemaOwnership {
string owner = 1; // Owner name or identifier
string team = 2; // Team responsible for the schema
string contact_email = 3; // Contact email for schema issues
bool read_only = 4; // If true, schema cannot be updated
}
message SchemaMetadata {
string schema_id = 1; // Unique identifier
string schema_version = 2; // Semantic version (e.g., "1.2.3")
string schema_hash = 3; // SHA256 hash of schema content
int64 registered_at = 4; // Unix timestamp of initial registration
int64 updated_at = 5; // Unix timestamp of last update
SchemaOwnership ownership = 6;
string openapi_version = 7; // Detected OpenAPI version (e.g., "3.0.0")
int32 endpoint_count = 8; // Number of endpoints in schema
}
message RegisterSchemaRequest {
string schema_id = 1;
string schema_content = 2; // OpenAPI YAML or JSON
string schema_version = 3; // Semantic version
SchemaOwnership ownership = 4;
bool check_compatibility = 5;
}
message RegisterSchemaResponse {
bool success = 1;
string message = 2;
SchemaMetadata metadata = 3; // Metadata for the registered schema
repeated BreakingChange breaking_changes = 4; // Breaking changes if check_compatibility was true
}
message GetSchemaRequest {
string schema_id = 1;
string schema_version = 2; // Optional: specific version (empty = latest)
}
message GetSchemaResponse {
bool found = 1;
SchemaMetadata metadata = 2;
string schema_content = 3; // The actual schema content
}
message ListSchemasRequest {
int32 page_size = 1; // Max results per page (default 100)
string page_token = 2; // Token for pagination
string owner = 3; // Optional: filter by owner
string team = 4; // Optional: filter by team
}
message ListSchemasResponse {
repeated SchemaMetadata schemas = 1;
string next_page_token = 2; // Token for next page (empty if no more)
int32 total_count = 3; // Total number of schemas matching filter
}
// ===== Validation =====
message InteractionRequest {
string schema_id = 1;
RequestData request = 2;
ResponseData response = 3;
string schema_version = 4; // Optional: validate against specific version
}
message RequestData {
string method = 1; // GET, POST, etc.
string path = 2; // /users/123
map headers = 3;
string body = 4; // JSON body as string
}
message ResponseData {
int32 status_code = 1;
map headers = 2;
string body = 3; // JSON body as string
}
message ValidationResult {
bool valid = 1;
repeated string errors = 2;
string validated_against_version = 3;
string validated_against_hash = 4;
}
message ValidateProducerRequest {
string schema_id = 1; // Schema to validate against
string schema_version = 2; // Optional: specific schema version
string method = 3; // HTTP method (GET, POST, etc.)
string path = 4; // API path with actual values (e.g., /users/123)
ResponseData response = 5; // The response to validate
RequestData request = 6; // Optional: request context (for path param extraction)
}
// ===== Schema Comparison =====
message CompareSchemasRequest {
string schema_id = 1;
string old_version = 2; // Version to compare from (empty = previous)
string new_version = 3; // Version to compare to (empty = latest)
}
message CompareSchemasResponse {
bool compatible = 1; // True if no breaking changes
repeated BreakingChange breaking_changes = 2;
SchemaMetadata old_schema = 3;
SchemaMetadata new_schema = 4;
}
message BreakingChange {
BreakingChangeType type = 1;
string path = 2; // API path affected (e.g., "/users/{id}")
string method = 3; // HTTP method affected (e.g., "POST")
string description = 4; // Human-readable description
string old_value = 5; // Previous value (for context)
string new_value = 6; // New value (for context)
}
enum BreakingChangeType {
BREAKING_CHANGE_UNSPECIFIED = 0;
ENDPOINT_REMOVED = 1; // Endpoint path+method was removed
REQUIRED_FIELD_ADDED = 2; // Required field added to request body
TYPE_CHANGED = 3; // Field type changed incompatibly
REQUIRED_PARAMETER_ADDED = 4; // Required query/path/header param added
RESPONSE_SCHEMA_CHANGED = 5; // Response schema changed incompatibly
ENUM_VALUE_REMOVED = 6; // Enum value was removed
}
// ===== Fixtures =====
message GenerateFixtureRequest {
string schema_id = 1; // Schema to generate fixtures from
string method = 2; // HTTP method (GET, POST, etc.)
string path = 3; // API path (e.g., /users/{id})
int32 status_code = 4; // Response status code (0 = auto-select)
bool use_examples = 5; // Use schema examples when available
string content_type = 6; // Content type (default: application/json)
OutputType output_type = 7; // What to generate
}
enum OutputType {
OUTPUT_FIXTURE = 0; // Complete request/response pair (default)
OUTPUT_REQUEST = 1; // Request body only
OUTPUT_RESPONSE = 2; // Response only
}
message GenerateFixtureResponse {
bool success = 1;
string message = 2;
GeneratedFixture fixture = 3; // Full fixture (if output_type = FIXTURE)
string request_body = 4; // Request body JSON (if output_type = REQUEST)
GeneratedResponse response = 5; // Response (if output_type = RESPONSE)
}
message GeneratedFixture {
GeneratedRequest request = 1;
GeneratedResponse response = 2;
}
message GeneratedRequest {
string method = 1;
string path = 2;
map headers = 3;
string body = 4; // JSON body as string
}
message GeneratedResponse {
int32 status_code = 1;
map headers = 2;
string body = 3; // JSON body as string
}
message ListEndpointsRequest {
string schema_id = 1;
}
message ListEndpointsResponse {
repeated EndpointInfo endpoints = 1;
}
message EndpointInfo {
string method = 1; // HTTP method
string path = 2; // API path
string operation_id = 3; // OpenAPI operationId (if defined)
string summary = 4; // OpenAPI summary (if defined)
}
// ===== Consumer Registry =====
message ConsumerInfo {
string consumer_id = 1;
string consumer_version = 2;
string schema_id = 3;
string schema_version = 4;
string environment = 5; // dev, staging, prod
int64 registered_at = 6; // Unix timestamp
int64 last_validated_at = 7; // Unix timestamp
repeated EndpointUsage used_endpoints = 8;
}
message EndpointUsage {
string method = 1;
string path = 2;
repeated string used_fields = 3; // Fields used in response (e.g., ["email", "name"])
}
message RegisterConsumerRequest {
string consumer_id = 1;
string consumer_version = 2;
string schema_id = 3;
string schema_version = 4;
string environment = 5; // dev, staging, prod
repeated EndpointUsage used_endpoints = 6;
}
message RegisterConsumerResponse {
bool success = 1;
string message = 2;
ConsumerInfo consumer = 3;
}
message ListConsumersRequest {
string schema_id = 1; // Required: schema to query
string environment = 2; // Optional: filter by environment
}
message ListConsumersResponse {
repeated ConsumerInfo consumers = 1;
}
message DeregisterConsumerRequest {
string consumer_id = 1;
string schema_id = 2;
string environment = 3;
}
message DeregisterConsumerResponse {
bool success = 1;
string message = 2;
}
// ===== Deployment Safety =====
message CanIDeployRequest {
string schema_id = 1;
string new_version = 2;
string environment = 3;
}
message CanIDeployResponse {
bool safe_to_deploy = 1;
string summary = 2;
repeated BreakingChange breaking_changes = 3;
repeated ConsumerImpact affected_consumers = 4;
}
message ConsumerImpact {
string consumer_id = 1;
string consumer_version = 2;
string current_schema_version = 3; // Version consumer was tested against
string environment = 4;
bool will_break = 5; // True if consumer will be affected
repeated BreakingChange relevant_changes = 6; // Breaking changes affecting this consumer
}
```
## Configuration
### Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| CVT_PORT | 9550 | gRPC server port |
| CVT_METRICS_PORT | 9551 | Prometheus metrics port |
| LOG_LEVEL | info | Logging level (debug, info, warn, error) |
#### TLS
| Variable | Default | Description |
|----------|---------|-------------|
| CVT_TLS_ENABLED | false | Enable TLS |
| CVT_TLS_CERT_FILE | /certs/server.crt | Server certificate |
| CVT_TLS_KEY_FILE | /certs/server.key | Server private key |
| CVT_TLS_CA_FILE | | CA certificate for mTLS |
| CVT_TLS_CLIENT_AUTH | none | Client auth: none, request, require |
#### API Key Authentication
| Variable | Default | Description |
|----------|---------|-------------|
| CVT_API_KEY_ENABLED | false | Enable API key auth |
| CVT_API_KEYS | | Comma-separated API keys |
| CVT_API_KEYS_FILE | | Path to JSON config |
#### Storage
| Variable | Default | Description |
|----------|---------|-------------|
| CVT_STORAGE_ENABLED | false | Enable persistent storage |
| CVT_STORAGE_TYPE | sqlite | Storage backend: sqlite, postgres, memory |
| CVT_STORAGE_DSN | cvt.db | SQLite database path |
| CVT_POSTGRES_HOST | localhost | PostgreSQL host |
| CVT_POSTGRES_PORT | 5432 | PostgreSQL port |
| CVT_POSTGRES_USER | cvt | PostgreSQL user |
| CVT_POSTGRES_PASSWORD | | PostgreSQL password |
| CVT_POSTGRES_DB | cvt | PostgreSQL database |
| CVT_POSTGRES_SSLMODE | disable | PostgreSQL SSL mode |
| CVT_POSTGRES_DSN | | Full PostgreSQL DSN (alternative to individual host/port/user/password/db) |
| CVT_POSTGRES_MAX_CONNS | 25 | Maximum open PostgreSQL connections |
| CVT_STORAGE_CACHE_ENABLED | true | Enable in-memory cache |
| CVT_STORAGE_CACHE_MAX_SCHEMAS | 1000 | Max schemas in cache |
| CVT_VALIDATION_RETENTION_DAYS | 90 | Days to retain validation records |
### API Keys JSON Format
```json
{
"keys": [
{
"key": "cvt-dev-key-12345",
"name": "development",
"scopes": ["*"]
},
{
"key": "cvt-prod-key-abcde",
"name": "production",
"scopes": ["*"],
"expires_at": "2026-01-01T00:00:00Z"
}
]
}
```
## Common Workflows
### Consumer Contract Testing
#### Node.js
```typescript
await validator.registerSchemaWithVersion("user-api", "./openapi.json", "1.0.0");
const result = await validator.validate(
{ method: "GET", path: "/users/123", headers: {} },
{ statusCode: 200, body: { id: "123", name: "John", email: "john@example.com" } }
);
assert(result.valid);
await validator.registerConsumer({
consumerId: "order-service",
consumerVersion: "2.1.0",
schemaId: "user-api",
schemaVersion: "1.0.0",
environment: "dev",
usedEndpoints: [
{ method: "GET", path: "/users/{id}", usedFields: ["id", "name", "email"] }
]
});
```
#### Python
```python
validator.register_schema_with_version("user-api", "./openapi.json", "1.0.0")
result = validator.validate(
request={"method": "GET", "path": "/users/123", "headers": {}},
response={"status_code": 200, "body": {"id": "123", "name": "John", "email": "john@example.com"}}
)
assert result["valid"]
validator.register_consumer(RegisterConsumerOptions(
consumer_id="order-service",
consumer_version="2.1.0",
schema_id="user-api",
schema_version="1.0.0",
environment="dev",
used_endpoints=[
EndpointUsage(method="GET", path="/users/{id}", used_fields=["id", "name", "email"])
]
))
```
#### Go
```go
ctx := context.Background()
validator.RegisterSchemaWithVersion(ctx, "user-api", "./openapi.json", "1.0.0")
result, _ := validator.Validate(ctx,
cvt.ValidationRequest{Method: "GET", Path: "/users/123"},
cvt.ValidationResponse{StatusCode: 200, Body: map[string]any{
"id": "123", "name": "John", "email": "john@example.com",
}},
)
assert.True(t, result.Valid)
validator.RegisterConsumer(ctx, cvt.RegisterConsumerOptions{
ConsumerID: "order-service", ConsumerVersion: "2.1.0",
SchemaID: "user-api", SchemaVersion: "1.0.0", Environment: "dev",
UsedEndpoints: []cvt.EndpointUsage{
{Method: "GET", Path: "/users/{id}", UsedFields: []string{"id", "name", "email"}},
},
})
```
#### Java
```java
validator.registerSchemaWithVersion("user-api", "./openapi.json", "1.0.0");
ValidationResult result = validator.validate(
ValidationRequest.builder().method("GET").path("/users/123").build(),
ValidationResponse.builder().statusCode(200)
.body("{\"id\":\"123\",\"name\":\"John\",\"email\":\"john@example.com\"}").build()
);
assertTrue(result.isValid());
validator.registerConsumer(RegisterConsumerOptions.builder()
.consumerId("order-service").consumerVersion("2.1.0")
.schemaId("user-api").schemaVersion("1.0.0").environment("dev")
.usedEndpoints(List.of(
new EndpointUsage("GET", "/users/{id}", List.of("id", "name", "email"))
)).build());
```
### Deployment Safety Check
#### Node.js
```typescript
const result = await validator.canIDeploy("user-api", "2.0.0", "prod");
if (!result.safeToDeploy) {
console.error("UNSAFE TO DEPLOY:", result.summary);
for (const consumer of result.affectedConsumers) {
if (consumer.willBreak) {
console.error(`${consumer.consumerId} v${consumer.consumerVersion} will break:`);
for (const change of consumer.relevantChanges) {
console.error(` - ${change.type}: ${change.description}`);
}
}
}
process.exit(1);
}
```
#### Python
```python
result = validator.can_i_deploy("user-api", "2.0.0", "prod")
if not result["safe_to_deploy"]:
print("UNSAFE TO DEPLOY:", result["summary"])
for consumer in result["affected_consumers"]:
if consumer["will_break"]:
print(f" {consumer['consumer_id']} v{consumer['consumer_version']} will break:")
for change in consumer["relevant_changes"]:
print(f" - {change['type']}: {change['description']}")
sys.exit(1)
```
#### Go
```go
result, _ := validator.CanIDeploy(ctx, "user-api", "2.0.0", "prod")
if !result.SafeToDeploy {
fmt.Println("UNSAFE TO DEPLOY:", result.Summary)
for _, c := range result.AffectedConsumers {
if c.WillBreak {
fmt.Printf(" %s v%s will break:\n", c.ConsumerID, c.ConsumerVersion)
for _, change := range c.RelevantChanges {
fmt.Printf(" - %s: %s\n", change.Type, change.Description)
}
}
}
os.Exit(1)
}
```
#### Java
```java
CanIDeployResult result = validator.canIDeploy("user-api", "2.0.0", "prod");
if (!result.isSafeToDeploy()) {
System.err.println("UNSAFE TO DEPLOY: " + result.getSummary());
for (ConsumerImpact c : result.getAffectedConsumers()) {
if (c.isWillBreak()) {
System.err.println(" " + c.getConsumerId() + " v" + c.getConsumerVersion() + " will break:");
for (BreakingChange change : c.getRelevantChanges()) {
System.err.println(" - " + change.getType() + ": " + change.getDescription());
}
}
}
System.exit(1);
}
```
### Producer Testing Workflow
Validate that your API server's responses conform to the schema.
#### Express + Strict Mode
```typescript
import express from "express";
import { ContractValidator } from "@sahina/cvt-sdk";
import { createExpressMiddleware } from "@sahina/cvt-sdk/producer";
const app = express();
const validator = new ContractValidator("localhost:9550");
await validator.registerSchema("user-api", "./openapi.json");
app.use(createExpressMiddleware({
schemaId: "user-api",
validator,
mode: "strict", // Reject invalid requests
}));
app.get("/users/:id", (req, res) => {
res.json({ id: req.params.id, name: "John" });
});
```
#### FastAPI + Shadow Mode
```python
from fastapi import FastAPI
from cvt_sdk import ContractValidator
from cvt_sdk.producer import ProducerConfig, ValidationMode
from cvt_sdk.producer.adapters import ASGIMiddleware
app = FastAPI()
validator = ContractValidator("localhost:9550")
validator.register_schema("user-api", "./openapi.json")
app.add_middleware(ASGIMiddleware, config=ProducerConfig(
schema_id="user-api",
validator=validator,
mode=ValidationMode.SHADOW, # Async validation, metrics only
))
```
#### Go (Chi) + Warn Mode
```go
import (
"net/http"
"github.com/go-chi/chi/v5"
cvt "github.com/sahina/cvt/sdks/go/cvt"
"github.com/sahina/cvt/sdks/go/cvt/producer"
producerAdapters "github.com/sahina/cvt/sdks/go/cvt/producer/adapters"
)
validator, _ := cvt.NewValidator("localhost:9550")
validator.RegisterSchema(ctx, "user-api", "./openapi.json")
r := chi.NewRouter()
r.Use(producerAdapters.ChiMiddleware(producer.Config{
SchemaID: "user-api",
Validator: validator,
Mode: producer.ModeWarn, // Log violations, allow requests
}))
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]any{"id": chi.URLParam(r, "id"), "name": "John"})
})
```
#### Spring + Warn Mode
```java
@Configuration
public class CvtConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
ContractValidator validator = new ContractValidator("localhost:9550");
validator.registerSchema("user-api", "./openapi.json");
registry.addInterceptor(new SpringInterceptor(ProducerConfig.builder()
.schemaId("user-api")
.validator(myValidator)
.mode(ValidationMode.WARN) // Log violations, allow requests
.build()))
.addPathPatterns("/api/**");
}
}
```
### HTTP Client Adapter Workflow
Transparently validate HTTP interactions and capture them for consumer registration.
#### Node.js with Axios
```typescript
import axios from "axios";
import { ContractValidator } from "@sahina/cvt-sdk";
import { createAxiosAdapter } from "@sahina/cvt-sdk/adapters";
const validator = new ContractValidator("localhost:9550");
await validator.registerSchema("user-api", "./openapi.json");
const api = axios.create({ baseURL: "http://localhost:3000" });
const adapter = createAxiosAdapter({ validator, axios: api });
// Run your tests — all HTTP calls are validated automatically
const users = await api.get("/users");
const user = await api.post("/users", { name: "Jane" });
// Auto-register as consumer from captured interactions
await validator.registerConsumerFromInteractions(
adapter.getInteractions(),
{ consumerId: "order-service", consumerVersion: "1.0.0", schemaId: "user-api", schemaVersion: "1.0.0", environment: "dev" }
);
```
#### Python with Requests
```python
from cvt_sdk import ContractValidator
from cvt_sdk.adapters import create_validating_session
validator = ContractValidator("localhost:9550")
validator.register_schema("user-api", "./openapi.json")
session = create_validating_session(validator)
# Run your tests — all HTTP calls are validated automatically
response = session.get("http://localhost:3000/users/123")
response = session.post("http://localhost:3000/users", json={"name": "Jane"})
# Auto-register as consumer
validator.register_consumer_from_interactions(
interactions=session.get_interactions(),
options={"consumer_id": "order-service", "consumer_version": "1.0.0",
"schema_id": "user-api", "schema_version": "1.0.0", "environment": "dev"}
)
```
#### Go with http.Client
```go
validator, _ := cvt.NewValidator("localhost:9550")
validator.RegisterSchema(ctx, "user-api", "./openapi.json")
rt := adapters.NewValidatingRoundTripper(adapters.RoundTripperConfig{
Validator: validator,
AutoValidate: true,
})
client := &http.Client{Transport: rt}
// Run your tests — all HTTP calls are validated
resp, _ := client.Get("http://localhost:3000/users/123")
// Auto-register as consumer
validator.RegisterConsumerFromInteractions(ctx, rt.GetInteractions(),
cvt.AutoRegisterConfig{
ConsumerID: "order-service", ConsumerVersion: "1.0.0",
Environment: "dev", SchemaVersion: "1.0.0", SchemaID: "user-api",
})
```
#### Java with OkHttp
```java
ContractValidator validator = new ContractValidator("localhost:9550");
validator.registerSchema("user-api", "./openapi.json");
OkHttpContractAdapter adapter = new OkHttpContractAdapter(validator);
OkHttpClient client = adapter.attach(new OkHttpClient.Builder()).build();
// Run your tests — all HTTP calls are validated
Response resp = client.newCall(new Request.Builder()
.url("http://localhost:3000/users/123").build()).execute();
// Auto-register as consumer
validator.registerConsumerFromInteractions(adapter.getInteractions(),
AutoRegisterConfig.builder()
.consumerId("order-service").consumerVersion("1.0.0")
.schemaId("user-api").schemaVersion("1.0.0").environment("dev")
.build());
```
### Auto-Registration Workflow
Build consumer registrations automatically from recorded HTTP interactions.
#### Node.js
```typescript
import { ContractValidator } from "@sahina/cvt-sdk";
import { createAxiosAdapter } from "@sahina/cvt-sdk/adapters";
const adapter = createAxiosAdapter({ validator, axios: api });
// Run integration tests — interactions are captured automatically
await api.get("/users/123");
await api.post("/users", { name: "Jane" });
// Dry run: inspect what would be registered
const consumerData = validator.buildConsumerFromInteractions(
adapter.getInteractions(),
{ consumerId: "order-service", consumerVersion: "1.0.0", schemaId: "user-api", environment: "dev" }
);
console.log("Endpoints used:", consumerData.usedEndpoints);
// Register consumer from captured interactions
await validator.registerConsumerFromInteractions(
adapter.getInteractions(),
{ consumerId: "order-service", consumerVersion: "1.0.0", schemaId: "user-api", schemaVersion: "1.0.0", environment: "dev" }
);
```
#### Python
```python
from cvt_sdk import ContractValidator
from cvt_sdk.adapters import create_validating_session
session = create_validating_session(validator)
# Run integration tests — interactions are captured automatically
session.get("http://localhost:3000/users/123")
session.post("http://localhost:3000/users", json={"name": "Jane"})
# Dry run: inspect what would be registered
consumer_data, err = validator.build_consumer_from_interactions(
interactions=session.get_interactions(),
options={"consumer_id": "order-service", "consumer_version": "1.0.0",
"schema_id": "user-api", "environment": "dev"}
)
# Register consumer from captured interactions
validator.register_consumer_from_interactions(
interactions=session.get_interactions(),
options={"consumer_id": "order-service", "consumer_version": "1.0.0",
"schema_id": "user-api", "schema_version": "1.0.0", "environment": "dev"}
)
```
#### Go
```go
rt := adapters.NewValidatingRoundTripper(adapters.RoundTripperConfig{
Validator: validator, AutoValidate: true,
})
client := &http.Client{Transport: rt}
// Run integration tests — interactions are captured automatically
client.Get("http://localhost:3000/users/123")
// Dry run: inspect what would be registered
opts, _ := validator.BuildConsumerFromInteractions(ctx, rt.GetInteractions(),
cvt.AutoRegisterConfig{
ConsumerID: "order-service", ConsumerVersion: "1.0.0",
SchemaID: "user-api", Environment: "dev",
})
fmt.Println("Endpoints used:", opts.UsedEndpoints)
// Register consumer from captured interactions
validator.RegisterConsumerFromInteractions(ctx, rt.GetInteractions(),
cvt.AutoRegisterConfig{
ConsumerID: "order-service", ConsumerVersion: "1.0.0",
SchemaID: "user-api", SchemaVersion: "1.0.0", Environment: "dev",
})
```
#### Java
```java
OkHttpContractAdapter adapter = new OkHttpContractAdapter(validator);
OkHttpClient client = adapter.attach(new OkHttpClient.Builder()).build();
// Run integration tests — interactions are captured automatically
client.newCall(new Request.Builder().url("http://localhost:3000/users/123").build()).execute();
// Dry run: inspect what would be registered
AutoRegisterUtils.BuildResult consumerData = validator.buildConsumerFromInteractions(
adapter.getInteractions(),
AutoRegisterConfig.builder()
.consumerId("order-service").consumerVersion("1.0.0")
.schemaId("user-api").environment("dev").build());
// Register consumer from captured interactions
validator.registerConsumerFromInteractions(adapter.getInteractions(),
AutoRegisterConfig.builder()
.consumerId("order-service").consumerVersion("1.0.0")
.schemaId("user-api").schemaVersion("1.0.0").environment("dev").build());
```
### CI/CD Integration
#### GitHub Actions
```yaml
name: Contract Validation
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
services:
cvt:
image: ghcr.io/sahina/cvt:latest
ports:
- 9550:9550
steps:
- uses: actions/checkout@v4
- name: Wait for CVT server
run: cvt wait --server localhost:9550 --timeout 60
- name: Register schema
run: cvt register-schema my-api ./openapi.yaml --version ${{ github.sha }}
- name: Check for breaking changes
run: |
cvt register-schema my-api ./openapi.yaml \
--check-compatibility --fail-on-breaking --json
- name: Validate test fixtures
run: |
for fixture in tests/fixtures/*.json; do
cvt validate --schema ./openapi.yaml --interaction "$fixture"
done
- name: Run SDK tests
run: npm test
- name: Check deployment safety
if: github.ref == 'refs/heads/main'
run: |
cvt can-i-deploy --schema my-api \
--version ${{ github.sha }} --env staging --json
```
#### Shell Script for CI
```bash
#!/bin/bash
set -euo pipefail
CVT_SERVER="${CVT_SERVER:-localhost:9550}"
SCHEMA_ID="${SCHEMA_ID:-my-api}"
VERSION="${VERSION:-$(git rev-parse --short HEAD)}"
# Wait for server
cvt wait --server "$CVT_SERVER" --timeout 120 -q
# Register schema with breaking change check
cvt register-schema "$SCHEMA_ID" ./openapi.yaml \
--version "$VERSION" \
--server "$CVT_SERVER" \
--check-compatibility \
--fail-on-breaking
# Validate all fixture files
for fixture in tests/fixtures/*.json; do
echo "Validating $fixture..."
cvt validate --schema ./openapi.yaml --interaction "$fixture"
done
# Check deployment safety
cvt can-i-deploy \
--schema "$SCHEMA_ID" \
--version "$VERSION" \
--env "${TARGET_ENV:-staging}" \
--server "$CVT_SERVER"
echo "All checks passed!"
```
### Test Fixture Generation
#### Node.js
```typescript
const endpoints = await validator.listEndpoints();
for (const ep of endpoints) {
const fixture = await validator.generateFixture(ep.method, ep.path, { useExamples: true });
console.log(`${ep.method} ${ep.path} request:`, fixture.request.body);
console.log(`${ep.method} ${ep.path} response:`, fixture.response.body);
}
// Generate just a response
const response = await validator.generateResponse("GET", "/users/123");
// Generate just a request body
const body = await validator.generateRequestBody("POST", "/users");
```
#### Python
```python
endpoints = validator.list_endpoints()
for ep in endpoints:
fixture = validator.generate_fixture(ep["method"], ep["path"],
GenerateOptions(use_examples=True))
print(f"{ep['method']} {ep['path']} request: {fixture['request']['body']}")
print(f"{ep['method']} {ep['path']} response: {fixture['response']['body']}")
# Generate just a response
response = validator.generate_response("GET", "/users/123")
# Generate just a request body
body = validator.generate_request_body("POST", "/users")
```
#### Go
```go
endpoints, _ := validator.ListEndpoints(ctx)
for _, ep := range endpoints {
fixture, _ := validator.GenerateFixture(ctx, ep.Method, ep.Path,
&cvt.GenerateOptions{UseExamples: true})
fmt.Printf("%s %s request: %v\n", ep.Method, ep.Path, fixture.Request.Body)
fmt.Printf("%s %s response: %v\n", ep.Method, ep.Path, fixture.Response.Body)
}
// Generate just a response
response, _ := validator.GenerateResponse(ctx, "GET", "/users/123", nil)
// Generate just a request body
body, _ := validator.GenerateRequestBody(ctx, "POST", "/users", nil)
```
#### Java
```java
List endpoints = validator.listEndpoints();
for (EndpointInfo ep : endpoints) {
GeneratedFixture fixture = validator.generateFixture(ep.getMethod(), ep.getPath(),
GenerateOptions.builder().useExamples(true).build());
System.out.println(ep.getMethod() + " " + ep.getPath() + " request: " + fixture.getRequest().getBody());
System.out.println(ep.getMethod() + " " + ep.getPath() + " response: " + fixture.getResponse().getBody());
}
// Generate just a response
GeneratedResponse response = validator.generateResponse("GET", "/users/123", null);
// Generate just a request body
Object body = validator.generateRequestBody("POST", "/users", null);
```
### Mock Testing Workflow
Test consumers against schema-generated mock responses — no real API needed.
#### Node.js with Mock Fetch
```typescript
import { ContractValidator } from "@sahina/cvt-sdk";
import { createMockFetch } from "@sahina/cvt-sdk/adapters";
const validator = new ContractValidator("localhost:9550");
await validator.registerSchema("user-api", "./openapi.json");
const mockFetch = createMockFetch(validator, { cache: true });
// Tests use mock responses generated from the schema
const response = await mockFetch("/users/123");
const user = await response.json();
expect(user).toHaveProperty("id");
expect(user).toHaveProperty("name");
```
#### Python with Mock Session
```python
from cvt_sdk import ContractValidator
from cvt_sdk.adapters import create_mock_session
validator = ContractValidator("localhost:9550")
validator.register_schema("user-api", "./openapi.json")
mock = create_mock_session(validator, cache=True)
response = mock.get("/users/123")
user = response.json()
assert "id" in user
assert "name" in user
```
#### Go with Mock Client
```go
mockClient := adapters.NewMockClient(validator, adapters.WithCache())
resp, _ := mockClient.Get("http://api.example.com/users/123")
defer resp.Body.Close()
// Response body is generated from the OpenAPI schema
```
#### Java with Mock OkHttp
```java
OkHttpClient mockClient = MockInterceptor.createMockClient(validator,
MockConfig.builder().cache(true).build());
Response response = mockClient.newCall(new Request.Builder()
.url("http://api.example.com/users/123").build()).execute();
// Response body is generated from the OpenAPI schema
```
## Deployment
### Docker
```bash
# Pull and run
docker pull ghcr.io/sahina/cvt:latest
docker run -p 9550:9550 -p 9551:9551 ghcr.io/sahina/cvt:latest
# With environment variables
docker run -p 9550:9550 \
-e CVT_STORAGE_ENABLED=true \
-e CVT_STORAGE_TYPE=sqlite \
-e LOG_LEVEL=debug \
ghcr.io/sahina/cvt:latest
```
### Docker Compose (Full Stack)
```bash
# Start server + PostgreSQL + Prometheus + Grafana
docker compose up -d
# Check health
cvt wait --server localhost:9550
```
## Ports
| Port | Service |
|------|---------|
| 9550 | gRPC server |
| 9551 | Prometheus metrics |
| 9091 | Prometheus UI (Docker) |
| 3000 | Grafana UI (Docker) |
| 5432 | PostgreSQL (Docker) |
## Package Registries
| SDK | Package | Registry |
|-----|---------|----------|
| Node.js | `@sahina/cvt-sdk` | [npm](https://www.npmjs.com/package/@sahina/cvt-sdk) |
| Python | `cvt-sdk` | [PyPI](https://pypi.org/project/cvt-sdk/) |
| Go | `github.com/sahina/cvt/sdks/go` | [pkg.go.dev](https://pkg.go.dev/github.com/sahina/cvt/sdks/go) |
| Java | `io.github.sahina:cvt-sdk` | [Maven Central](https://central.sonatype.com/artifact/io.github.sahina/cvt-sdk) |
| Docker | `ghcr.io/sahina/cvt` | [GHCR](https://ghcr.io/sahina/cvt) |