Skip to content

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

  1. Overview
  2. Request Envelope
  3. Response Envelope
  4. Error Handling
  5. Health Check Protocol
  6. Versioning Policy
  7. Language Implementations
  8. 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:

  1. Accept requests for their current version
  2. Optionally support older versions for backward compatibility
  3. 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

  1. Always validate input: Don't trust the gateway's validation alone
  2. Use proper error codes: Match errors to the standard codes when possible
  3. Include request_id: Always echo back the request_id for tracing
  4. Implement health check: Required for the gateway to detect module availability
  5. Version your responses: Always include the version in responses
  6. Log everything: Use structured logging for debugging
  7. Set appropriate timeouts: Don't let requests hang indefinitely

For Client Developers

  1. Always check status: Always verify the "status" field is "success"
  2. Handle errors gracefully: Parse error responses and display user-friendly messages
  3. Track requests: Use request_id for debugging and support
  4. Check versions: Be aware of module versions for compatibility
  5. 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
  }
}