Powered by JSON-RPC 2.0
Overview
It’s hard to imagine a modern software system which doesn’t either consume or
provide a means of
machine to machine communication. In fact most applications both provide and depend on various
web services, APIs etc.
The idea of web services or online APIs isn’t new. There is a dedicated
style of the software architecture which focuses on methodologies,
communication protocols and structures of the applications which are built
around the concept of web services. It is called
service-oriented architecture (SOA)
and is also applied in the field of software design.
Many different protocols and methodologies have been developed for
composing web services and providing descriptions for them. This article
is not intended to classify them, but for simplicity we can divide the
specifications into two groups:
RPC
family and
REST
APIs.
The software developed in our company doesn’t differ from the others in
the matter of web service utilization. Our systems are integrated to
dozens of APIs and provide web-services for even more other applications.
The main protocol used for web services is
JSON-RPC 2.0.
The power of JSON-RPC
JSON-RPC is of course not the only RPC protocol, but it is
known for its simplicity and brief specification. As it can be implied
from its name, the protocol uses
JSON
as the data serialization format. JSON is widely used and is
considered to be more concise compared to other human readable
serialization formats like
XML. Although XML is a mature technology and supports extension
natively (which can be “guessed” from its name), JSON got
popularity along with wide adaptation of JavaScript. JSON is also
used in the majority of REST API as well.
JSON-RPC powers the
language server protocol
(LSP). Many programming languages implement it for integration with text
editors and IDEs in order to provide better experience during application
development.
The few notable features of the protocol are being transport agnostic (can
be used with raw TCP/UDP connections and is great with web sockets),
support of
notifications
(client's lack of interest in the corresponding Response object) and
batch request processing
functionality.
{
"jsonrpc": "2.0",
"method": "subtract",
"params": [
42,
23
],
"id": 1
}
Fig. 1. JSON-RPC2.0 request example
The simplicity of the protocol allowed us to implement both client and
server libraries ourselves, thus not depending on third-party libraries.
Almost all software developed in VXSoft provide JSON-RPC based
web-services both for administrative purposes and for integrating with other
systems.
import uuid
import requests
requests.post(
'http://localhost/json-rpc',
json={
'jsonrpc': '2.0',
'method': 'subtract',
'params': [42, 23],
'id': str(uuid.uuid4())
}
)
Fig. 2. Simple JSON-RPC2.0 request using Python's "requests" library
One of the disadvantages of the JSON-RPC 2.0 specification is the
lack of rules for describing a service like
WSDL for
SOAP. Another limitation of
the protocol is missing
security standards.
To solve the problem with a service description, after some research we
stumbled upon OpenRPC specification. It
is specifically designed for describing and documenting
JSON-RPC 2.0 based web service and is using
JSON schema for defining
requests and responses. By utilizing the OpenRPC we can generate
machine readable structures of our services and then automatically use them
in client libraries for composing requests and validating responses. The
description of a service documented using OpenRPC specification can
be discovered by the
/openrpc.json
path of the URL.
{
"openrpc": "1.3.1",
"info": {
"title": "My JSONRPC2.0 service",
"version": "0.0.1",
"license": {
"name": ""
}
},
"servers": [
{
"url": "/json-rpc"
}
],
"methods": [
]
}
Fig. 3. Basic OpenRPC definition
A service description may include generic information about a web-service
including its version and license, the URL path of the service and
definition of its methods.
A method definition by an OpenRPC specification includes it's name,
summary and the structure of both request and response objects. It may
also provide properties for indicating whether a certain method is
deprecated or not and how the method is going to receive it's arguments
(positional and named arguments). The request and response objects are
defined using JSON schema.
{
"methods": [
{
"name": "start_transaction",
"summary": "Start signing transaction",
"tags": [
{
"name": "t"
}
],
"params": [
{
"name": "title",
"required": true,
"schema": {
"type": "object"
},
"deprecated": false
},
{
"name": "callback",
"required": true,
"schema": {
"type": "string"
},
"deprecated": false
},
{
"name": "file",
"required": true,
"schema": {
"type": "object"
},
"deprecated": false
},
{
"name": "signers",
"required": true,
"schema": {
"type": "array"
},
"deprecated": false
}
],
"result": {},
"deprecated": false,
"paramStructure": "by-name"
}
]
}
Fig. 4. A JSON-RPC method definition using OpenRPC specification
Fig.4 presents a description of
start_transaction
method with four arguments: title,
callback, file, signers. It also indicated that the
method isn't deprecated ("deprecated": false
) and that the
arguments must be passed by name ("paramStructure": "by-name"
). The result
key must contain the structure of the
JSON-RPC 2.0 result object.
The definition of all methods provided by a single service is generated
from our code automatically. As our code base is in Python we can
use the dynamic nature of the language and existing type
annotations
for composing the OpenRPC document. A (very) simplified and (very) limited
implementation of a method definition generation may look like this:
from enum import StrEnum
from typing import Callable
def openrpc_spec(
functions: list[Callable],
title: str,
version: str,
server: str
) -> dict:
# mapping between Python's built-in types and JSON types
typedef: dict = {
int: "number",
float: "number",
str: "string",
bool: "boolean",
list: "array",
tuple: "array",
dict: "object",
}
methods: list = []
for func in functions:
summary: str = func.__doc__ or ''
params: list = []
for param, typ in func.__annotations__.items():
# skip the result definition
if param == 'return':
continue
if jtype := typedef.get(typ):
schema = {"type": jtype}
elif issubclass(typ, StrEnum):
schema = {"type": "string", "enum": tuple(typ)}
else:
schema = {"type": str(typ)}
# generate the definition for the method
params.append({
"name": param,
"required": True,
"schema": schema,
"deprecated": False,
})
methods.append({
"name": func.__name__,
"summary": summary and summary.strip(),
"tags": [{"name": "t"}],
"params": params,
"result": '{}',
"deprecated": False,
"errors": [],
"paramStructure": "by-name", # by-position | either
})
return {
"openrpc": "1.3.1",
"info": {
"title": title,
"version": version,
"license": {"name": ""},
},
"servers": [{"url": server}],
"methods": methods,
}
def square(number: int) -> None:
...
def append(value: str, collection: list, unique: bool) -> list:
...
print(
openrpc_spec(
[square, append],
'My JSON-RPC 2.0 service',
'0.0.1',
'/json-rpc'
)
)
Fig. 5. OpenRPC method definition generation in Python
The code in fig.5 tries to map built-in Python data types to
JSON schema defined types and also tries to detect some types like
StrEnum
. It is simplified for the article and obviously lacks
the response object structure generation and nested object definition, but
it's a step in the right direction and may work for simple services. We
run a slightly modified and complete version of this code for generating
openrpc.json
files.
Conclusion
Being able to easily provide and consume a web service or an API is an
essential part of the modern IT infrastructure. In the world of complex
standards and protocols it's always a nice thing to have a simpler
approach for solving a problem. For our company that simpler approach for
web service development was choosing JSON-RPC 2.0 as our main
protocol. Spicing it with OpenRPC specification we are able to provide web
services to hundreds of our clients who can easily integrate to the system
we are developing.
Of course there isn't a silver bullet for solving all issues or a unified
technology for all aspects of life. Thus everyone must find their own
niche toolbox.
No comments: