JSON Contract Specification¶
The JSON contract is the backbone of PolyAPI's polyglot architecture. This document provides a comprehensive specification for all inter-module communication, enabling modules written in different programming languages to communicate seamlessly.
Table of Contents¶
- Overview
- Request Envelope
- Response Envelope
- Error Handling
- Health Check Protocol
- Versioning Policy
- Language Implementations
- Best Practices
Overview¶
Every module, regardless of the programming language, must communicate with the FastAPI gateway exclusively through HTTP + JSON following this contract. The contract ensures:
- Language Agnosticism: Any language with JSON support can participate
- Type Safety: Consistent data structures across all modules
- Debuggability: Request tracking via unique request IDs
- Error Standardization: Consistent error format across all modules
Communication Flow¶
Client Gateway Module
│ │ │
│ POST /endpoint │ │
│─────────────────────>│ │
│ │ POST /module_endpoint │
│ │────────────────────────>│
│ │ │
│ │ ResponseEnvelope │
│ │<────────────────────────│
│ ResponseEnvelope │ │
│<─────────────────────│ │
Request Envelope¶
The request envelope is the format clients use to communicate with modules through the gateway. The gateway wraps the client's payload with additional metadata before forwarding to the module.
Schema¶
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"module": "sort",
"version": "1.0.0",
"payload": {
"items": [5, 2, 8, 1],
"order": "asc"
}
}
Field Definitions¶
| Field | Type | Required | Description |
|---|---|---|---|
request_id |
string | No | UUID v4 string. Auto-generated by gateway if not provided. Used for request tracking. |
module |
string | Yes | Module identifier (e.g., "sort", "filter", "transform"). Must match a registered module. |
version |
string | Yes | Module version in semver format (e.g., "1.0.0"). Used for version compatibility checks. |
payload |
object | Yes | Module-specific payload data. The content and structure depend on the module's API. |
Validation Rules¶
| Field | Validation Rule |
|---|---|
request_id |
Must be a valid UUID v4 if provided. If empty or invalid, gateway will generate a new one. |
module |
Must be a non-empty string matching a registered module in the gateway configuration. |
version |
Must follow semver format (MAJOR.MINOR.PATCH). Example: "1.0.0", "2.1.3", "1.0.0-beta" |
payload |
Must be a valid JSON object. Content validation is performed by the module-specific payload schema at the gateway level. |
Client-Side Request Format¶
When making requests to the gateway, clients only need to provide the payload:
{
"payload": {
"items": [5, 2, 8, 1],
"order": "asc"
}
}
The gateway automatically adds:
- request_id (if not provided)
- module (from the route)
- version (from module configuration)
Response Envelope¶
All responses, whether success or error, must use the response envelope format. This ensures consistent error handling and response parsing across all clients.
Success Response Schema¶
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"module": "sort",
"version": "1.0.0",
"status": "success",
"data": {
"sorted": [1, 2, 5, 8],
"item_type": "number",
"count": 4
},
"error": null
}
Error Response Schema¶
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"module": "sort",
"version": "1.0.0",
"status": "error",
"data": null,
"error": {
"code": "EMPTY_INPUT",
"message": "input array is empty",
"details": {
"field": "items"
}
}
}
Field Definitions¶
| Field | Type | Required | Description |
|---|---|---|---|
request_id |
string | Yes | Matches the request_id from the incoming request. Enables request-response correlation. |
module |
string | Yes | Module identifier. Should match the module handling the request. |
version |
string | Yes | Module version. Should match the version in the request or the module's current version. |
status |
string | Yes | Either "success" or "error". Indicates the overall outcome of the request. |
data |
object/null | Yes | Response data on success, null on error. Structure is module-specific. |
error |
object/null | Yes | Error details on error, null on success. Must be null for successful responses. |
Error Object Structure¶
| Field | Type | Required | Description |
|---|---|---|---|
code |
string | Yes | Error code from the registry. See Error Codes for standard codes. |
message |
string | Yes | Human-readable error message. Should be actionable and help developers understand the issue. |
details |
object/null | No | Additional error context. Can include field names, validation errors, or internal debugging info. |
Error Handling¶
Proper error handling is crucial for a good developer experience. Modules must return appropriate error responses for various failure scenarios.
Standard Error Codes¶
| Code | HTTP Status | Description | Typical Use Case |
|---|---|---|---|
INVALID_JSON |
400 | Malformed JSON in request body | JSON parsing failures |
INVALID_INPUT |
400 | Request payload failed validation | Wrong data types, missing required fields |
EMPTY_INPUT |
400 | Required input is empty or missing | Empty arrays where items required |
INVALID_TYPE |
400 | Wrong data type for a field | String where number expected |
UNKNOWN_FIELD |
400 | Unknown field in payload | Extra fields not in schema |
PROCESSING_ERROR |
500 | Internal processing error | Module-specific errors |
MODULE_UNREACHABLE |
503 | Module is not available | Gateway can't reach module |
TIMEOUT |
504 | Request timed out | Module took too long to respond |
Example Error Responses¶
Validation Error¶
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"module": "sort",
"version": "1.0.0",
"status": "error",
"data": null,
"error": {
"code": "INVALID_INPUT",
"message": "field 'order' must be either 'asc' or 'desc'",
"details": {
"field": "order",
"received": "invalid_value",
"expected": ["asc", "desc"]
}
}
}
Missing Required Field¶
{
"request_id": "550e8400-e29b-41d4-a716-446655440001",
"module": "sort",
"version": "1.0.0",
"status": "error",
"data": null,
"error": {
"code": "INVALID_INPUT",
"message": "field 'items' is required",
"details": {
"field": "items",
"reason": "missing required field"
}
}
}
Internal Error¶
{
"request_id": "550e8400-e29b-41d4-a716-446655440002",
"module": "sort",
"version": "1.0.0",
"status": "error",
"data": null,
"error": {
"code": "PROCESSING_ERROR",
"message": "failed to sort items",
"details": {
"reason": "comparison function failed",
"internal_error": "nil pointer dereference"
}
}
}
Health Check Protocol¶
Every module must implement a health check endpoint for the gateway to verify its availability.
Request¶
GET /health
No request body or headers required.
Response Schema¶
{
"status": "ok",
"module": "sort",
"version": "1.0.0"
}
Field Definitions¶
| Field | Type | Description |
|---|---|---|
status |
string | "ok" when healthy, "error" or "unhealthy" when not |
module |
string | Module name identifier |
version |
string | Module version in semver format |
Example Health Check¶
curl http://localhost:8081/health
Response:
{
"status": "ok",
"module": "sort",
"version": "1.0.0"
}
Versioning Policy¶
Semantic Versioning¶
PolyAPI follows Semantic Versioning 2.0.0:
- MAJOR (x.0.0): Breaking changes. The contract structure or behavior has fundamentally changed.
- MINOR (1.x.0): New features (backward compatible). Adding new optional fields or endpoints.
- PATCH (1.0.x): Bug fixes. No changes to the contract, only corrections to behavior.
Version Format¶
MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]
Examples:
- 1.0.0 - Initial release
- 1.1.0 - New feature added
- 1.1.1 - Bug fix
- 2.0.0 - Breaking change
- 2.0.0-beta - Pre-release
- 2.0.0+20230301 - Build metadata
Contract Evolution¶
Backward-Compatible Changes (Minor version bump)¶
- Adding new optional fields to the payload
- Adding new optional fields to the response
- Adding new error codes
- Adding new modules
- Adding new endpoints
Breaking Changes (Major version bump)¶
- Removing or renaming required fields
- Changing field types
- Changing enum values
- Removing error codes
- Changing response structure
- Removing endpoints
Version Negotiation¶
The gateway passes the module version in the request. Modules should:
- Accept requests for their current version
- Optionally support older versions for backward compatibility
- Return their current version in the response
Language Implementations¶
Go¶
package main
import (
"encoding/json"
"log"
"net/http"
)
// RequestEnvelope represents the incoming JSON contract request
type RequestEnvelope struct {
RequestID string `json:"request_id"`
Module string `json:"module"`
Version string `json:"version"`
Payload map[string]interface{} `json:"payload"`
}
// Error represents error information in the response
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Details interface{} `json:"details,omitempty"`
}
// ResponseEnvelope represents the JSON contract response
type ResponseEnvelope struct {
RequestID string `json:"request_id"`
Module string `json:"module"`
Version string `json:"version"`
Status string `json:"status"`
Data interface{} `json:"data,omitempty"`
Error *Error `json:"error,omitempty"`
}
// HealthResponse represents the health check response
type HealthResponse struct {
Status string `json:"status"`
Module string `json:"module"`
Version string `json:"version"`
}
func main() {
http.HandleFunc("/your_endpoint", handleRequest)
http.HandleFunc("/health", handleHealth)
log.Fatal(http.ListenAndServe(":8082", nil))
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(HealthResponse{
Status: "ok",
Module: "your_module",
Version: "1.0.0",
})
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
var req RequestEnvelope
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
sendError(w, req.RequestID, "INVALID_JSON", "malformed json", nil)
return
}
// Validate required fields
if req.Payload == nil {
sendError(w, req.RequestID, "EMPTY_INPUT", "payload is required", nil)
return
}
// Process the request
result := processData(req.Payload)
// Send success response
resp := ResponseEnvelope{
RequestID: req.RequestID,
Module: "your_module",
Version: "1.0.0",
Status: "success",
Data: result,
Error: nil,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func processData(payload map[string]interface{}) interface{} {
// Your processing logic here
return map[string]interface{}{"result": "processed"}
}
func sendError(w http.ResponseWriter, requestID, code, message string, details interface{}) {
resp := ResponseEnvelope{
RequestID: requestID,
Module: "your_module",
Version: "1.0.0",
Status: "error",
Data: nil,
Error: &Error{Code: code, Message: message, Details: details},
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(resp)
}
Python¶
from flask import Flask, request, jsonify
from pydantic import BaseModel, Field
from typing import Any, Optional, Literal
import uuid
app = Flask(__name__)
# Request/Response Models
class RequestEnvelope(BaseModel):
request_id: str = ""
module: str
version: str
payload: dict[str, Any]
class ErrorInfo(BaseModel):
code: str
message: str
details: Optional[dict[str, Any]] = None
class ResponseEnvelope(BaseModel):
request_id: str
module: str
version: str
status: Literal["success", "error"]
data: Optional[Any] = None
error: Optional[ErrorInfo] = None
@app.route('/health')
def health():
return jsonify({
"status": "ok",
"module": "your_module",
"version": "1.0.0"
})
@app.route('/your_endpoint', methods=['POST'])
def handle_request():
try:
data = request.json
# Parse and validate request
req = RequestEnvelope(**data)
# Process the request
result = process_data(req.payload)
# Return success response
resp = ResponseEnvelope(
request_id=req.request_id or str(uuid.uuid4()),
module="your_module",
version="1.0.0",
status="success",
data=result,
error=None
)
return jsonify(resp.model_dump(exclude_none=True))
except Exception as e:
# Return error response
resp = ResponseEnvelope(
request_id=data.get('request_id', str(uuid.uuid4())),
module="your_module",
version="1.0.0",
status="error",
data=None,
error=ErrorInfo(
code="PROCESSING_ERROR",
message=str(e)
)
)
return jsonify(resp.model_dump(exclude_none=True)), 400
def process_data(payload: dict[str, Any]) -> dict[str, Any]:
# Your processing logic here
return {"result": "processed"}
if __name__ == '__main__':
app.run(port=8082)
Rust¶
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize, Deserialize)]
pub struct RequestEnvelope {
#[serde(default)]
pub request_id: String,
pub module: String,
pub version: String,
pub payload: serde_json::Value,
}
#[derive(Serialize, Deserialize)]
pub struct ErrorInfo {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
pub struct ResponseEnvelope {
pub request_id: String,
pub module: String,
pub version: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorInfo>,
}
#[derive(Serialize, Deserialize)]
pub struct HealthResponse {
pub status: String,
pub module: String,
pub version: String,
}
fn process_data(payload: serde_json::Value) -> serde_json::Value {
serde_json::json!({ "result": "processed" })
}
fn send_error(request_id: &str, code: &str, message: &str) -> ResponseEnvelope {
ResponseEnvelope {
request_id: request_id.to_string(),
module: "your_module".to_string(),
version: "1.0.0".to_string(),
status: "error".to_string(),
data: None,
error: Some(ErrorInfo {
code: code.to_string(),
message: message.to_string(),
details: None,
}),
}
}
Best Practices¶
For Module Developers¶
- Always validate input: Don't trust the gateway's validation alone
- Use proper error codes: Match errors to the standard codes when possible
- Include request_id: Always echo back the request_id for tracing
- Implement health check: Required for the gateway to detect module availability
- Version your responses: Always include the version in responses
- Log everything: Use structured logging for debugging
- Set appropriate timeouts: Don't let requests hang indefinitely
For Client Developers¶
- Always check status: Always verify the "status" field is "success"
- Handle errors gracefully: Parse error responses and display user-friendly messages
- Track requests: Use request_id for debugging and support
- Check versions: Be aware of module versions for compatibility
- Validate responses: Validate responses against expected schemas
Example Request/Response Pairs¶
Sort Module: String Sorting¶
Request:
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"module": "sort",
"version": "1.0.0",
"payload": {
"items": ["banana", "apple", "cherry"],
"order": "asc"
}
}
Response:
{
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"module": "sort",
"version": "1.0.0",
"status": "success",
"data": {
"sorted": ["apple", "banana", "cherry"],
"item_type": "string",
"count": 3
},
"error": null
}
Sort Module: Number Sorting (Descending)¶
Request:
{
"request_id": "550e8400-e29b-41d4-a716-446655440001",
"module": "sort",
"version": "1.0.0",
"payload": {
"items": [5, 2, 8, 1],
"order": "desc"
}
}
Response:
{
"request_id": "550e8400-e29b-41d4-a716-446655440001",
"module": "sort",
"version": "1.0.0",
"status": "success",
"data": {
"sorted": [8, 5, 2, 1],
"item_type": "number",
"count": 4
},
"error": null
}
Error: Empty Input¶
Request:
{
"request_id": "550e8400-e29b-41d4-a716-446655440002",
"module": "sort",
"version": "1.0.0",
"payload": {
"items": [],
"order": "asc"
}
}
Response:
{
"request_id": "550e8400-e29b-41d4-a716-446655440002",
"module": "sort",
"version": "1.0.0",
"status": "error",
"data": null,
"error": {
"code": "EMPTY_INPUT",
"message": "input array is empty",
"details": null
}
}
Related Documentation¶
- Error Codes - Standard error codes reference
- Modules Overview - Module architecture
- Creating a Module - Build your first module
- Gateway Configuration - Configure modules in gateway