Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

jsonrpc_service!

Use jsonrpc_service! when you want an HTTP JSON-RPC API with typed request and response payloads, generated server dispatch, generated Rust clients, and optional OpenRPC output.

Dependencies And Features

Put the macro in the shared API definition crate. If you want server and client outputs to stay optional, expose API-crate features that forward to the macro crate features and enable the runtime dependencies those generated surfaces refer to.

[dependencies]
ras-jsonrpc-macro = { version = "0.2.0", default-features = false }
ras-jsonrpc-types = "0.1.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
schemars = "1.0.0-alpha.20"
ras-transport-core = { version = "0.1.0", optional = true }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
ras-jsonrpc-core = { version = "0.1.2", optional = true }
axum = { version = "0.8", optional = true }
tokio = { version = "1.0", features = ["full"], optional = true }

[features]
default = []
server = ["ras-jsonrpc-macro/server", "dep:ras-jsonrpc-core", "dep:axum", "dep:tokio"]
client = ["ras-jsonrpc-macro/reqwest", "ras-transport-core/reqwest"]

Server binaries then depend on my-api with features = ["server"]; clients depend on the same API crate with features = ["client"]. The generated code itself is selected by the ras-jsonrpc-macro features, not by generated consumer-crate cfg attributes.

The macro crate’s client feature emits the generated client types and build_with_transport(...). Its reqwest feature also emits the default reqwest-backed build(). If a crate only injects a custom transport, forward ras-jsonrpc-macro/client plus dep:ras-transport-core instead of ras-jsonrpc-macro/reqwest.

Define The Service

use ras_jsonrpc_macro::jsonrpc_service;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct SignInRequest {
    pub email: String,
    pub password: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct SignInResponse {
    pub token: String,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct UserProfile {
    pub user_id: String,
}

jsonrpc_service!({
    service_name: UserService,
    openrpc: true,
    methods: [
        UNAUTHORIZED sign_in(SignInRequest) -> SignInResponse,
        WITH_PERMISSIONS(["user"]) get_profile(()) -> UserProfile,
        WITH_PERMISSIONS(["admin"] | ["support", "users:write"]) disable_user(String) -> (),
    ]
});

The Rust method name is the JSON-RPC wire method unless a versioned method block sets an explicit wire name.

Implement The Generated Trait

Protected methods receive &AuthenticatedUser before their request payload:

struct UserServiceImpl;

impl UserServiceTrait for UserServiceImpl {
    async fn sign_in(
        &self,
        request: SignInRequest,
    ) -> Result<SignInResponse, Box<dyn std::error::Error + Send + Sync>> {
        todo!("verify credentials and issue token")
    }

    async fn get_profile(
        &self,
        user: &ras_jsonrpc_core::AuthenticatedUser,
        _request: (),
    ) -> Result<UserProfile, Box<dyn std::error::Error + Send + Sync>> {
        Ok(UserProfile {
            user_id: user.user_id.clone(),
        })
    }
}

Build The Router

let rpc = UserServiceBuilder::new(UserServiceImpl)
    .base_url("/rpc")
    .auth_provider(my_auth_provider)
    .build()?;

let app = axum::Router::new().merge(rpc);

The generated server extracts bearer credentials from Authorization, can be configured for secure session cookies, routes by JSON-RPC method name, parses typed params, checks auth, and converts handler errors into JSON-RPC error responses.

Use The Generated Rust Client

Enable the shared API crate’s client feature in the crate that makes outbound calls:

[dependencies]
my-api = { path = "../api", default-features = false, features = ["client"] }

The generated client calls methods by their Rust names and sends the correct JSON-RPC wire method internally.

let mut client = UserServiceClientBuilder::new("http://localhost:3000/rpc")
    .with_timeout(std::time::Duration::from_secs(10))
    .build()?;

let signed_in = client
    .sign_in(SignInRequest {
        email: "alice@example.com".to_string(),
        password: "correct horse battery staple".to_string(),
    })
    .await?;

client.set_bearer_token(Some(signed_in.token));

let profile = client.get_profile(()).await?;

client
    .disable_user("user-123".to_string())
    .await?;

For browser/WASM clients, use the same generated client with a browser URL and set the bearer token on a cloned client before protected calls:

let client = UserServiceClientBuilder::new("/rpc").build()?;

let mut authed = client.clone();
authed.set_bearer_token(Some(token));

let profile = authed.get_profile(()).await?;

OpenRPC And Clients

With openrpc: true, the macro generates:

pub fn generate_userservice_openrpc() -> serde_json::Value;
pub fn generate_userservice_openrpc_to_file() -> Result<(), std::io::Error>;

Request and response types must implement schemars::JsonSchema for OpenRPC generation. The generated document includes schemas, method names, auth metadata, permission metadata, and version metadata.

The API crate’s client feature emits typed Rust methods for the current operation names and, when a method declares versioned compatibility, for the legacy Rust method aliases too. Each generated method still sends the configured wire method name, so old and new clients can coexist while the server migrates requests at the API boundary.

Browser clients can compile to WASM when the API crate dependency is enabled with features = ["client"] for wasm32.

See the runnable service in examples/basic-jsonrpc and the WASM client usage in examples/wasm-ui-demo.