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 be 2.0,

  • method: the "endpoint"/operation to call, business names must not start with rpc. (it is reserved for internal RPC methods),

  • params (optional): it is the method parameters, they can be a list, in such a case the params type is an array and parameters are ordered - it is called by-position - or an object which means the parameters will be named (root attributes being the name of the parameters) - it is called by-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)
}
1 The identifier of the request,
2 The jsonrpc version as required by the specification,
3 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"
    }]
}
1 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"
        }
    }
}
1 The method now takes a parameter named user
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).

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 be 2.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 request id to enable to associate the response to the request.

The error field is a JSON object which contains the following fields:

  • code: an integer identifying the error which happent. Code from -32768 to -32000 are reserved by the specification and have a special meaning. For example -32600 means the request was invalid. You can find the list on the specification page,

  • 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"
            }
        ]
    }
]
1 The role listing (id=2) response comes faster than the user listing because it actually failed and we get the related error,
2 The user listing (id=1) suceeded and we get the list of users as result.
examples stay simple in the context of this post but in real applications the listing would use as usual a pagination structure ({total,items} for example).

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"
                }
            ]
        }
    }
]
1 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,
2 We request the enriched bulk endpoint to patch params by injecting in its users 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:

  1. Persist some entity,

  2. Persist some other entity and link it to previous stored entity (by primary key for example),

  3. 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: