JSON-RPC presentation
JSON-RPC defines a protocol. It enables to unify the business logic under a single pattern with a standard stucture accross the whole system.
JSON-RPC scope
JSON-RPC defines a lightweight RPC protocol. In other words, it defines the way to do a request and the way the response will be sent back to the caller.
As a high level RPC (Remote Procedure Call) solution, it is transport agnostic which means it can be used over HTTP (the most common), websocket, plain socket or even a messaging system like Kafka or ActiveMQ.
Its format is, as the name suggests, plain JSON.
Anatomy of a JSON-RPC exchange
JSON-RPC Request
The request shape is defined as a JSON with the following list of attributes:
-
jsonrpc
: protocol version, as of today it must be2.0
, -
method
: the "endpoint"/operation to call, business names must not start withrpc.
(it is reserved for internal RPC methods), -
params
(optional): it is the method parameters, they can be a list, in such a case theparams
type is an array and parameters are ordered - it is calledby-position
- or an object which means the parameters will be named (root attributes being the name of the parameters) - it is calledby-name
, -
id
(optional): it can be omitted, null, a number - normally only integers - or a string. It is coupled with the response to associate the response to its request (same value) but over most connected transports it can be omitted even if not recommended. This is more important for bulk calls (we'll see it later).
Here is an example of simple, no parameter request:
{
"id": "aszdz-dzdek-79263", (1)
"jsonrpc": "2.0", (2)
"method": "list-users" (3)
}
- The identifier of the request,
-
The
jsonrpc
version as required by the specification, - The method "id" to call.
A parameterized method using ordered parameter(s) can look like this:
{
"id": "aszdz-dzdek-79263",
"jsonrpc": "2.0",
"method": "save-user",
"params": [{ (1)
"name": "John Doe"
}]
}
- The method takes one parameter of type object (representing the user to save)
The same method using named parameter will look like:
{
"id": "aszdz-dzdek-79263",
"jsonrpc": "2.0",
"method": "save-user",
"params": {
"user": { (1)
"name": "John Doe"
}
}
}
-
The method now takes a parameter named
user
TIP
|
depending the JSON-RPC framework you use you will be able to use both parameter options or a single one. |
Notifications
Notifications are plain JSON-RPC requests but they never have an id
attribute. It is supposed to notify the client does not care about the response (client push the data but does not expect an answer).
TIP
|
most JSON-RPC frameworks are very tolerant over this high level concept, ensure to check what yours does/enables. |
JSON-RPC Response
JSON-RPC reponses are very similar to JSON-RPC requests and define the following attributes:
-
jsonrpc
: protocol version, as of today it must be2.0
, -
result
(only on success): when the call suceeds it contains the response to the request, -
error
(only on failures): it is an object (we'll define the structure just after) which is present when the call failed, -
id
: same value as the requestid
to enable to associate the response to the request.
The error
field is a JSON object which contains the following fields:
-
code
, -
message
: a short description representing the error - a bit like an exception message, -
data
: a free JSON value giving context about the error (it can be a string, number, object, array, ...).
Here is a sample success response to previous save-user
request:
{
"id": "aszdz-dzdek-79263",
"jsonrpc": "2.0",
"result": {
"id": 1234,
"name": "John Doe",
"created": "2021-05-05T14:43:00Z",
"updated": "2021-05-05T14:43:00Z"
}
}
And here is an error response sample:
{
"id": "aszdz-dzdek-79263",
"jsonrpc": "2.0",
"error": {
"code": 1001,
"message": "User already exists.",
"data": {
"id": 1234,
"name": "John Doe",
"created": "2021-05-05T14:43:00Z",
"updated": "2021-05-05T14:43:00Z"
}
}
}
Bulk handling
To optimize the network usage, JSON-RPC specification enabled to bulk the requests. This is one of the cases where using id
in requests becomes very important because the server can process the requests concurrently in some cases.
Except when the request is invalid - and the response will be a standard error, the request and response will be an array of request/responses as seen previously. The only trick to keep in mind is to match the response based on the identifier of the request and not the order in the array which is not guaranteed by the specification.
Here is an example of request trying to list users and roles through the same request:
[
{
"id": "1",
"jsonrpc": "2.0",
"method": "list-users"
},
{
"id": "2",
"jsonrpc": "2.0",
"method": "list-roles"
}
]
And here is a potential response:
[
{ (1)
"id": "2",
"jsonrpc": "2.0",
"error": {
"code": 1101,
"message": "Database connection lost."
}
},
{ (2)
"id": "1",
"jsonrpc": "2.0",
"result": [
{
"id": 1234,
"name": "John Doe",
"created": "2021-05-05T14:43:00Z",
"updated": "2021-05-05T14:43:00Z"
}
]
}
]
-
The role listing (
id
=2) response comes faster than the user listing because it actually failed and we get the related error, -
The user listing (
id
=1) suceeded and we get the list of users asresult
.
NOTE
|
examples stay simple in the context of this post but in real applications the listing would use as usual a pagination structure ( |
Going further
Implementations generally provide a MethodRegistry
or whatever API enabling you to do a call based on a request object.
Coupled with the fact parsing a JSON is quite easy, it enabled you to add enriched methods enabling to do more.
A common example is a bulk like endpoint chaining the calls with a preprocessing of the "next" call. This case is really common these days and enables to give the caller some orchestration capabilities (à la GraphQL but more powerful and easier in terms of implementation and integration with any framework/stack/language).
To illustrate this example, let's assume we will enrich the bulk handling by supporting a /$extension/patch
additional entry in the request object. The idea is to iterate over each request of the incoming array, executes the JSON-RPC method and stores the response in an object (we can modelize it a JSON with an attribute /responses
which is the list/array of the previous responses). Before executing the JSON-RPC method it will apply the JSON-Patch in /$extension/patch
to the request and execute the method with the result JSON instead of the raw incoming one.
Here is an example of request with such a logic:
[
{
"id": "1",
"jsonrpc": "2.0",
"method": "list-users"
},
{
"id": "2",
"jsonrpc": "2.0",
"method": "list-roles-for-user",
"params": {}, (1)
"$extension": {
"patch": [ (2)
{
"op": "COPY",
"from": "/responses/0/result",
"path": "/params/users"
}
]
}
}
]
-
We assume
list-roles-for-user
needs a list of users as input but we set an empty parameter object because we will populate it from the previous call, -
We request the enriched bulk endpoint to patch
params
by injecting in itsusers
attribute the previous execution result (list of users).
The response would be exactly the same as in previous example but the big difference and gain of such a technic is that we chained two calls and the second call used the result from the previous call - to filter the roles to list from the list of users.
A more complex case would use exactly the same technic to:
-
Persist some entity,
-
Persist some other entity and link it to previous stored entity (by primary key for example),
-
Trigger some action on the last persisted entity.
All that in a single call and without having to do a specific endpoint, just CRUD for the entities and the action endpoint.
It really opens doors to the client/frontend applications without requiring any investment in terms of backend - no customization of the server but no proxy-like server too to add the missing endpoints for the frontend application.
Conclusion
This enriched bulk method is really just a small example of what JSON-RPC enables.
What is important to keep in mind is that it is a very simple protocol which, being based on JSON, can be supported by any server and client. It is really one of the most polyglot solution as of today and outperform GraphQL or alternaitve a lot on that aspect.
The other very nice thing with JSON-RPC is that since it is JSON and just about a command oriented registry (the method implementations), it is very easy to extend it with more advanced features. We saw how to enrich it in terms of orchestration but you can also add field filtering quite easily (most trivial implementation is about filtering a JSON) or even optimize bulk-ed requests by collapsing them (doing pushdown on the bulk request, for example merging two SQL requests in one).
The last point is that it is transport agnostic so you can use it:
-
over HTTP (1, 2, 3) indeed,
-
over websockets,
-
but also over messaging systems (notifications and
id
usage makes a lot of sense there) including Apache ActiveMQ or Apache Kafka, -
or even to implement a command line interface (CLI) since the options will be the request attributes but it is a command oriented design - we do it at Yupiik to leverage our existing backend on some products.
So last word is that when you want a very flexible protocol you can invest a bit in your company and be sure it will match any transport, performance and feature, JSON-RPC is a very good bet in today's ecosystem.
From the same author:
In the same category: