How JSON-RPC Methods Are Registered, Dispatched, and Schema-Documented in OpenHuman

OpenHuman uses a domain-driven registration pattern where controllers are collected into a global registry in src/core/all.rs, dispatched via O(1) hash-map lookups in src/core/jsonrpc.rs, and documented through aggregated schema endpoints exposed at /_rpc/schemas.

OpenHuman implements a JSON-RPC 2.0 API surface built on the Axum HTTP framework, utilizing a three-stage lifecycle for method handling. This architecture separates concerns between domain-specific controller definitions, centralized routing logic, and automatic schema generation to support both machine-readable APIs and human-friendly documentation.

Domain-Driven Registration in src/core/all.rs

Each functional domain—such as voice, web_chat, or wallet—exports two critical functions that enable the JSON-RPC system to discover its capabilities. The first function returns concrete handler implementations, while the second provides machine-readable documentation.

Domain modules expose these functions via pub use statements in their respective mod.rs files. For example, the voice domain in src/openhuman/voice/mod.rs re-exports:

pub use schemas::{
    all_voice_controller_schemas,
    all_voice_registered_controllers,
    voice_schemas,
};

The central aggregator at src/core/all.rs collects these vectors into a global registry:

let mut controllers = Vec::new();
controllers.extend(openhuman::voice::all_voice_registered_controllers());
controllers.extend(openhuman::web_chat::all_web_channel_registered_controllers());
// … repeat for each gated domain …

This pattern populates the REGISTERED_CONTROLLERS global registry, making every method available to the dispatcher at startup.

O(1) Request Dispatch via src/core/jsonrpc.rs

Incoming HTTP POST requests hit the router constructed by build_core_http_router in src/core/jsonrpc.rs. This Axum router defines two critical routes:

pub fn build_core_http_router() -> Router {
    Router::new()
        .route("/", post(handle_jsonrpc))
        .route("/_rpc/schemas", get(serve_schemas))
}

The handle_jsonrpc function executes the dispatch logic:

  1. Parse the request body as a JSON-RPC envelope containing jsonrpc: "2.0", id, method, and params.
  2. Perform a hash-map lookup of the method string in REGISTERED_CONTROLLERS.
  3. Invoke the stored handler with signature fn(&Value) -> Result<Value, JsonRpcError>.
  4. Wrap the result or error into a proper JSON-RPC response.

Because the registry is built statically at startup, method resolution achieves O(1) complexity regardless of the total number of registered endpoints.

Self-Documenting Schema Endpoints

OpenHuman automatically generates API documentation through the /_rpc/schemas endpoint, which aggregates schema definitions from every domain. The serve_schemas function in src/core/jsonrpc.rs assembles these into a single JSON array:

async fn serve_schemas() -> impl IntoResponse {
    let mut schemas = Vec::new();
    schemas.extend(openhuman::voice::all_voice_controller_schemas());
    schemas.extend(openhuman::web_chat::all_web_channel_controller_schemas());
    // … add each domain …
    Json(schemas)
}

Each ControllerSchema struct contains:

  • method – The full JSON-RPC name (e.g., openhuman.voice_stt_dispatch).
  • description – Human-readable purpose of the method.
  • params – JSON Schema fragment for request validation.
  • result – JSON Schema fragment for response structure.

The React frontend consumes this endpoint at startup to build the tool catalogue and populate the "RPC Explorer" interface, ensuring the UI always reflects the current API surface.

Practical Example: Adding a New JSON-RPC Method

To expose a new capability through the JSON-RPC interface, you must register both the handler and its schema. Here is the complete workflow for adding a voice operation:

1. Implement the controller in the domain file (e.g., src/openhuman/voice/controllers.rs):

pub async fn my_new_action(params: Value) -> Result<Value, JsonRpcError> {
    // …implementation…
    Ok(json!({ "ok": true }))
}

2. Register the handler in the domain's registry (e.g., src/openhuman/voice/schemas/registry.rs):

pub fn all_voice_registered_controllers() -> Vec<RegisteredController> {
    vec![
        RegisteredController::new(
            "voice.my_new_action",
            "Performs a custom voice operation",
            my_new_action,
        ),
    ]
}

3. Define the schema in the same registry file:

pub fn all_voice_controller_schemas() -> Vec<ControllerSchema> {
    vec![
        ControllerSchema::new(
            "voice.my_new_action",
            "Performs a custom voice operation",
            json!({ "type": "object", "properties": {} }),
            json!({ "type": "object", "properties": { "ok": { "type": "boolean" } } })
        ),
    ]
}

After recompiling, the method becomes immediately available for dispatch at the root RPC endpoint and automatically appears in the /_rpc/schemas documentation.

Summary

  • Registration occurs through domain-specific functions like all_voice_registered_controllers() that are aggregated in src/core/all.rs into a global registry.
  • Dispatch relies on O(1) hash-map lookups in src/core/jsonrpc.rs, where handle_jsonrpc routes requests to the appropriate controller.
  • Schema Documentation is served via the /_rpc/schemas endpoint, which merges ControllerSchema definitions from all domains to support automatic UI generation.
  • The architecture decouples domain logic from transport concerns while maintaining type safety through Rust's strict function signatures.

Frequently Asked Questions

How does OpenHuman achieve O(1) routing for JSON-RPC methods?

OpenHuman builds a static hash map at startup in src/core/all.rs by collecting all RegisteredController instances from domain modules. The handle_jsonrpc function in src/core/jsonrpc.rs performs a direct lookup of the method name string in this map, ensuring constant-time dispatch regardless of how many methods are registered.

What is the difference between all_voice_registered_controllers and all_voice_controller_schemas?

all_voice_registered_controllers returns a vector of RegisteredController structs containing the actual Rust function pointers used for execution, while all_voice_controller_schemas returns ControllerSchema structs containing JSON Schema metadata for documentation and validation. Both must be updated when adding new methods to ensure the system can both execute and describe the endpoint.

How can I expose a new domain's JSON-RPC methods in OpenHuman?

Create the domain's mod.rs file with two public functions following the naming convention all_<domain>_registered_controllers() and all_<domain>_controller_schemas(). Import and call these functions in src/core/all.rs to append them to the global vectors, and the methods will automatically appear in both the dispatch registry and the schema documentation endpoint.

Where does OpenHuman serve the JSON-RPC schema documentation?

The schema documentation is served at the /_rpc/schemas GET endpoint defined in src/core/jsonrpc.rs. This endpoint aggregates all domain schemas into a single JSON array that clients—including the React frontend and CLI tools—consume to discover available methods and their parameter requirements.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →