pyjsonrpc2
A correct, transport-agnostic Python implementation of the JSON-RPC 2.0 protocol.
Key features
- Fully complies with the JSON-RPC 2.0 specification
- Works over any transport: the library does no I/O
- Works from several threads: the client is thread safe, and the server is thread safe when your registered methods are also thread safe
- Accepts JSON input as
str,bytes,bytearrayormemoryview - Declares complete type hints (passes
pyreflyon thestrictpreset) - Covers every line with unit tests
- Follows semantic versioning
Installation
pyjsonrpc2 requires Python 3.11 or later.
Use pip to install the package:
pip install pyjsonrpc2
Usage
JsonRpcServer turns the raw bytes of a request into the raw bytes of a response. JsonRpcClient turns a method call into the raw bytes of a request and a Future that receives the answer. Neither half does any I/O.
The examples/ directory has more in-depth examples.
Server
Basic server creation
from pyjsonrpc2.server import JsonRpcServer, rpc_method, JsonRpcError
# Create a basic server
server = JsonRpcServer()
Method registration patterns
These are the main patterns to register RPC methods. See examples/server/registering_methods.py.
- Give a mapping of names to callables to the constructor:
server = JsonRpcServer({"get_version": lambda: "1.0"})
- Decorate the methods of a subclass:
class MathServer(JsonRpcServer):
@rpc_method
def square(self, x):
return x**2
@rpc_method(name="cube")
def calculate_cube(self, x):
return x**3
server = MathServer()
- Register the decorated methods of another object. An optional
prefixkeeps two objects that have the same method names separate:
class MathUtils:
@rpc_method
def multiply(self, a, b):
return a * b
server.add_object(MathUtils(), prefix="utils.") # Registers "utils.multiply"
- Add one method with a decorator:
@server.add_method
def add(a, b):
return a + b
- Add a method under a different name:
def sub(a, b):
return a - b
server.add_method(sub, name="subtract")
- Add a lambda function:
server.add_method(lambda a, b: a % b, name="modulo")
Every registration path refuses a callable that the server cannot use, with a ValueError. It refuses:
- an object that is not callable at all
- a coroutine function (
async def), becausecall()is synchronous and nothing awaits it - an async generator function
- a generator function
Wrap the call in a synchronous callable that returns an encodable value, and register that instead:
import asyncio
async def fetch(url): ...
# ValueError: Cannot register coroutine functions: 'fetch'
server.add_method(fetch)
# Register a synchronous callable that runs it instead
server.add_method(lambda url: asyncio.run(fetch(url)), name="fetch")
Reading and changing the registry
The methods property lists the registry, from RPC method name to callable. It gives a read-only view, so register through add_method() and add_object().
sorted(server.methods)
# ['add', 'cube', 'modulo', 'multiply', 'square', 'subtract']
"modulo" in server.methods
# True
remove_method() takes one name back out of the registry, and returns the callable that the name held. The server answers a later request for that name with Method not found (-32601). It raises KeyError for a name that the registry does not hold.
modulo = server.remove_method("modulo") # KeyError if it is not registered
server.add_method(modulo, name="mod") # The old name is free again
add_object() puts a prefix before every name that it registers, and remove_method() takes the registry name. Give it the prefix too:
server.add_object(MathUtils(), prefix="utils.")
server.remove_method("utils.multiply")
Error handling
The server handles errors as follows:
JsonRpcErrorcarries a custom code for an implementation-defined or an application-defined error- Any other Python exception becomes an Internal error (
-32603) response - An argument mismatch becomes an Invalid params (
-32602) response - An error can carry additional data in any JSON structure
- The server answers a protocol error itself, such as invalid JSON or a missing key
- The server logs an uncaught exception on the
pyjsonrpc2.serverlogger, at theERRORlevel and with a traceback
- Raise a custom implementation-defined error:
class AdvancedMathServer(JsonRpcServer):
@rpc_method
def divide(self, a, b):
if b == 0:
raise JsonRpcError(
code=-32000,
message="Division by zero",
data={"numerator": a, "denominator": b},
)
return a / b
- Use more than one error condition:
class AdvancedMathServer(JsonRpcServer):
@rpc_method
def factorial(self, n):
if not isinstance(n, int):
# Regular exceptions are caught and converted to Internal error responses
raise TypeError("n must be an integer")
if n < 0:
# Custom JSON-RPC errors with additional data
raise JsonRpcError(
code=-32001,
message="Invalid input for factorial",
data={"input": n, "reason": "Must be non-negative"},
)
# ... implementation ...
The data that you give to JsonRpcError must be JSON serializable. A return value must also be JSON serializable. The server catches a return value that is not, and answers with an Internal error. That error carries the serialization failure as its data.
Request execution
call() returns the encoded response as bytes. It returns None when the server owes the client no answer. There are two such cases: a single notification, and a batch that holds only notifications.
server.call('{"jsonrpc": "2.0", "method": "add", "params": [5, 3], "id": 1}')
# b'{"jsonrpc":"2.0","id":1,"result":8}'
server.call(b'{"jsonrpc": "2.0", "method": "subtract", "params": [5, 3], "id": 2}')
# b'{"jsonrpc":"2.0","id":2,"result":2}'
# A notification (no "id"): nothing is owed to the client
server.call('{"jsonrpc": "2.0", "method": "add", "params": [5, 3]}')
# None
# A batch is answered with an array of the responses its elements are owed
server.call(
'[{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 3},'
' {"jsonrpc": "2.0", "method": "modulo", "params": [7, 3], "id": 4}]'
)
# b'[{"jsonrpc":"2.0","id":3,"result":3},{"jsonrpc":"2.0","id":4,"result":1}]'
dumps_kwargs gives extra keyword arguments to orjson.dumps().
import orjson
server = JsonRpcServer(dumps_kwargs={"option": orjson.OPT_INDENT_2})
Client
Making calls
request() returns two things: the bytes to send, and the Future that receives the answer. The future stays pending until you give the response to handle(). handle() matches the response to its request by id.
from pyjsonrpc2.client import JsonRpcClient
client = JsonRpcClient()
request, future = client.request("subtract", 42, 23)
# request: b'{"jsonrpc":"2.0","method":"subtract","params":[42,23],"id":1}'
transport.send(request) # a socket, an HTTP request, a pipe, or anything else
client.handle(transport.recv())
future.result() # 19
Parameters are positional or named, exactly as in the protocol. *args becomes the "params" array. **kwargs becomes the "params" object. A call that gives both raises ValueError. The method name is positional-only, so a parameter with the name method also goes into "params". A method name that is not a string raises TypeError.
client.request("subtract", minuend=42, subtrahend=23)
client.request("subtract", 42, subtrahend=23) # ValueError
client.request(42) # TypeError
Notifications
A notification carries no id. The server owes no answer, and the client gives you no future. The client can match nothing against a notification, not even a failure.
client.notify("log", "hello")
# b'{"jsonrpc":"2.0","method":"log","params":["hello"]}'
Batch requests
A batch collects calls and encodes them as one payload. request() returns a future for its own element. notify() returns nothing. One response payload settles every future in the batch.
batch = client.batch()
total = batch.request("sum", 1, 2, 4)
batch.notify("log", "part of the batch")
difference = batch.request("subtract", 42, 23)
transport.send(batch.encode())
client.handle(transport.recv())
total.result() # 7
difference.result() # 19
encode() does not close a batch. You can add more calls and encode the batch again. encode() refuses an empty batch with ValueError, because the specification disallows it.
Handling responses
handle() accepts a single response or a batch, as JSON text or as its UTF-8 encoding. It accepts them in any order and from any thread. It returns the errors that the server sent under a null id, which belong to no request. This list is usually empty:
unattributed = client.handle(response)
A server that cannot parse a request has no id to answer under, so it answers with a null id. The client can match nothing against a null id. A guess would fail the wrong call, so handle() returns these errors to you instead.
The client logs a response that matches nothing else, then drops it. It uses the pyjsonrpc2.client logger at the WARNING level. Three examples are a late answer, a duplicate, and an id that you never sent.
Error handling
- An
"error"response raisesJsonRpcErrorout ofFuture.result(). That error carries the server'scode,messageanddata. The server raises the same class, so neither half must translate anything. - A response that is not a JSON-RPC response fails the future that it matches, with
InvalidResponseError. The caller of that one request is the one who hears about it. A response is malformed when:- the
"jsonrpc"version is wrong - it has both a
"result"and an"error" - it has neither of them
- the
handle()raisesInvalidResponseErroritself when the payload as a whole is unusable. It settles nothing in that case. A payload is unusable when:- it is not valid JSON
- it is not an object and not an array
- it is an empty array
- A response that belongs to no request and is not a well-formed error settles no future.
handle()raises nothing for it and does not return it. The client logs it and drops it. This group holds:- a response with no
id - a null
idthat carries a"result" - a null
idthat carries an unusable"error" - a batch element that is not an object
- a response with no
from pyjsonrpc2.client import InvalidResponseError, JsonRpcError
try:
future.result()
except JsonRpcError as e:
print(e.code, e.message, e.data)
except InvalidResponseError as e:
print("the server answered with something that is not JSON-RPC:", e)
A request is pending from the moment that the client builds it. This is true even if the request never reaches a transport. When a transport stops, nothing can answer the requests that are still pending. Release them, or their futures stay pending forever:
client.cancel_pending(ConnectionError("socket closed")) # returns how many were pending
client.cancel_pending() # cancels them instead. result() raises CancelledError
Client configuration
Both arguments are keyword-only. dumps_kwargs works as it does on the server. id_iterator replaces the source of request ids, which is itertools.count(1) by default. Use it for a server that is particular about the type of an id. The iterator must give JSON serializable values that are unique for the life of the client, and never None. The client refuses an id that already awaits a response, and raises ValueError. It does not lose the future that waits under that id.
import itertools
client = JsonRpcClient(id_iterator=(f"call-{n}" for n in itertools.count()))
Tests
To run tests, clone the repository, install the package in your environment and run the following command at the root of the repository:
python -m unittest
As a more robust alternative, you can install tox to automatically test across the supported python versions, then run:
tox -p
Issue tracker
Please report any bugs or enhancement ideas using the issue tracker.
License
pyjsonrpc2 is licensed under the terms of the MIT License.
Metadata
Release files for pyjsonrpc2 3.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyjsonrpc2-3.0.0.tar.gz | 19.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyjsonrpc2-3.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 39.5 kB
Release files / pyjsonrpc2-3.0.0.tar.gz
| Download URL | pyjsonrpc2-3.0.0.tar.gz |
|---|---|
| Size | 19.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e9e00642cdb907c8eb51efa93a98d7b28878bacdaac7c89f9f876a29ac0075d2
|
|
BLAKE2b-256 checksum How to use checksums |
0bf51e518a07b2f05635349934966182cd38c68d73b46effa9a07744cac6bc42
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency logRelease files / pyjsonrpc2-3.0.0-py3-none-any.whl
| Download URL | pyjsonrpc2-3.0.0-py3-none-any.whl |
|---|---|
| Size | 20.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c2f3929404873681670ccf89162b72b4053b9b2141ccb6e187b1f5238cfe795e
|
|
BLAKE2b-256 checksum How to use checksums |
5a45fce7c19676594840c91a54612f03626db4cdf5c1afab3bd2effdadfced5f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.
Transparency log