The Essence of JSON-RPC

Last update: 2023-03-31 21:19:23 +0800 +0800

so jsonrpc is nothing more than a json schema saying that please follow this structure, but you just do http+json body anyway

JSON-RPC is a small protocol for expressing remote procedure calls as JSON. A client names an operation, supplies its arguments, and optionally assigns an identifier that links the eventual response to the request.

Its essence is:

text
Call this named operation with these arguments,
then tell me either its result or its error.

A minimal request looks like this:

json
{
  "jsonrpc": "2.0",
  "id": 17,
  "method": "subtract",
  "params": {
    "minuend": 42,
    "subtrahend": 23
  }
}

The corresponding response is:

json
{
  "jsonrpc": "2.0",
  "id": 17,
  "result": 19
}

JSON-RPC does not define what subtract means, which methods exist, or the schema of their parameters and results. An application protocol built on JSON-RPC defines those details. JSON-RPC supplies only the common message envelope and call semantics.

Is It Just HTTP with a JSON Body? #

In many deployments, JSON-RPC is carried in an ordinary HTTP request:

http
POST /rpc HTTP/1.1
Host: calculator.example.com
Content-Type: application/json

{"jsonrpc":"2.0","id":17,"method":"subtract","params":{"minuend":42,"subtrahend":23}}

So at the transport level, yes: this is HTTP with a JSON body. But that description omits the convention that makes the body interoperable.

HTTP defines:

  • how a request reaches a server;
  • methods such as GET and POST;
  • URLs, headers, authentication, caching, and status codes; and
  • how bytes are transferred.

JSON defines:

  • how values such as objects, arrays, strings, and numbers are encoded.

JSON-RPC defines:

  • which remote operation is requested through method;
  • how arguments are supplied through params;
  • how concurrent calls are correlated through id;
  • whether a message is a request, response, notification, or error; and
  • the standard shape of successful and failed responses.

The layers therefore have different responsibilities:

text
Application methods:  subtract, initialize, SendMessage, tools/call
JSON-RPC:             method, params, id, result, error
Transport:            HTTP, WebSocket, stdio, or another byte channel
Serialization:        JSON

JSON-RPC itself is transport-independent. HTTP is common, but a protocol can carry the same messages over WebSocket, a persistent stream, standard input and output, or another transport.

The Four Message Shapes #

Request #

A request asks the receiver to invoke a method and return a response:

json
{
  "jsonrpc": "2.0",
  "id": "request-123",
  "method": "user.get",
  "params": {
    "userId": 42
  }
}

The required jsonrpc value identifies JSON-RPC 2.0. method is a string. params, when present, is an object for named arguments or an array for positional arguments. id is chosen by the caller.

Successful Response #

A successful response repeats the request ID and contains result:

json
{
  "jsonrpc": "2.0",
  "id": "request-123",
  "result": {
    "userId": 42,
    "name": "Ada"
  }
}

The caller uses id, not arrival order, to associate this response with its request. This matters when several requests are in flight concurrently.

Error Response #

A failed call contains error instead of result:

json
{
  "jsonrpc": "2.0",
  "id": "request-123",
  "error": {
    "code": -32602,
    "message": "Invalid params",
    "data": {
      "field": "userId"
    }
  }
}

Every error has a numeric code and textual message; data is optional. JSON-RPC reserves a range of codes for protocol-level failures:

CodeMeaning
-32700Parse error
-32600Invalid request
-32601Method not found
-32602Invalid parameters
-32603Internal error

Applications may define additional codes for domain failures. This creates a machine-readable error contract instead of making every client infer failure from arbitrary text.

Notification #

A notification is a request without an id:

json
{
  "jsonrpc": "2.0",
  "method": "cache.invalidate",
  "params": {
    "key": "user:42"
  }
}

The receiver does not send a response, including when processing fails. A notification therefore means “send and do not wait for a protocol-level reply,” not “guaranteed delivery.” Delivery, retries, acknowledgements, and durability depend on the transport and application protocol.

JSON-RPC Is a Message Model, Not a Complete API #

JSON-RPC deliberately leaves many questions unanswered:

  • Which methods are available?
  • What parameters and result types does each method use?
  • Is a method safe, idempotent, or state-changing?
  • How are versions negotiated?
  • How are clients authenticated and authorized?
  • How are large files transferred?
  • How are long-running operations represented?
  • Are notifications ordered or durable?
  • How are streaming results delivered?

A higher-level protocol must answer these questions. For example, the Model Context Protocol uses JSON-RPC messages but adds initialization, capability negotiation, tools, resources, prompts, and lifecycle rules. The Agent2Agent Protocol’s JSON-RPC binding maps A2A operations and data objects onto JSON-RPC calls. In both cases, JSON-RPC is plumbing rather than the entire contract.

This distinction is important:

text
JSON-RPC tells peers how to represent a call.
The application protocol tells peers what the call means.

JSON-RPC Compared with REST #

REST and JSON-RPC can both use HTTP and JSON, but they organize an API differently.

A REST-style API emphasizes resources and HTTP semantics:

http
POST /orders
GET /orders/42
DELETE /orders/42

A JSON-RPC API usually exposes one endpoint and places the operation in the body:

http
POST /rpc

{"jsonrpc":"2.0","id":1,"method":"order.create","params":{...}}
http
POST /rpc

{"jsonrpc":"2.0","id":2,"method":"order.get","params":{"orderId":42}}

The practical difference is not “HTTP versus no HTTP.” It is where the application semantics live:

ConcernREST styleJSON-RPC style
OperationHTTP method plus URLmethod field
TargetResource URLUsually one RPC endpoint
ArgumentsPath, query, headers, bodyparams
SuccessHTTP status plus representationresult
FailureHTTP status plus API-specific bodyStandard error envelope
CorrelationUsually one response per HTTP requestExplicit id

Neither style is universally superior. REST fits APIs naturally modeled as resources and benefits directly from HTTP caching, conditional requests, and generic intermediaries. JSON-RPC fits command-oriented interfaces, protocol methods, bidirectional connections, and systems where every message should share one compact envelope.

An HTTP JSON-RPC implementation still needs a documented mapping between HTTP and JSON-RPC failures. For example, malformed HTTP, failed authentication, and an unavailable server are transport-level failures, while an unknown RPC method is a JSON-RPC error. Collapsing both layers into one vague error model makes clients unreliable.

Batching #

JSON-RPC 2.0 allows a client to send an array of requests and notifications:

json
[
  {"jsonrpc":"2.0","id":1,"method":"user.get","params":{"userId":42}},
  {"jsonrpc":"2.0","id":2,"method":"user.get","params":{"userId":43}},
  {"jsonrpc":"2.0","method":"audit.touch","params":{"scope":"users"}}
]

The server returns responses for the two requests but not for the notification. Response order need not match request order, so IDs remain essential.

Batching reduces transport overhead, but it is not automatically a database transaction. The specification does not require calls to execute in order, atomically, or even sequentially. An application requiring those guarantees must define them explicitly.

What JSON-RPC Does Not Guarantee #

The presence of an id does not provide exactly-once execution. Consider:

text
Client sends payment.charge(id = 9).
Server charges the card.
Connection fails before the response arrives.
Client does not know whether the call ran.

Blindly retrying may execute the operation twice. Robust APIs need application-level idempotency keys, deduplication, or a way to query operation status. The JSON-RPC request ID correlates messages; it is not necessarily a business-operation identifier.

JSON-RPC also does not provide:

  • encryption;
  • authentication or authorization;
  • service discovery;
  • schemas or code generation;
  • retries and backoff;
  • cancellation;
  • flow control;
  • observability context;
  • streaming semantics; or
  • compatibility guarantees between API versions.

Those features come from the transport, an application protocol, supporting standards, or implementation conventions.

When JSON-RPC Is a Good Fit #

JSON-RPC is useful when:

  • the interface is naturally a set of named commands;
  • clients and servers need a small language-neutral envelope;
  • calls may share a persistent or bidirectional transport;
  • explicit request correlation is useful;
  • notifications are needed;
  • a higher-level protocol already defines methods and data models; or
  • the uniformity of one endpoint is more useful than resource-oriented HTTP semantics.

It is less compelling when:

  • the API is naturally a collection of cacheable resources;
  • standard HTTP behavior is central to the design;
  • browser and CDN integration should work with minimal custom logic;
  • rich schemas, generated clients, and service governance are required but no additional specification mechanism is planned; or
  • a mature domain-specific protocol already solves the problem.

The Essential Mental Model #

JSON-RPC is not an alternative network stack and not a complete distributed system. It is a thin, standardized vocabulary for remote calls:

text
Request:      id + method + params
Success:      id + result
Failure:      id + error
Notification: method + params, with no id and no response

When used over HTTP, it really is JSON in an HTTP body. Its value lies in the shared meaning of that body: both sides agree how to identify a call, route it to a named operation, correlate its response, and represent its failure. The remaining behavior belongs to the application protocol built on top.

Comments

Read or comment on GitHub (sign-in required to comment)