Repository: pymeasure/pyleco (language: python)

Issue:
RPCGenerator depends on private API
The RPCGenerator in #3 depends on the private API of `jsonrpc2-pyclient`, as the baseclass is intended to include some message transfer methods, which are incompatible with pyleco.


Discussion / hints:
The private API is `from jsonrpc2pyclient._irpcclient import IRPCClient`, which we inherit from.

Maybe useful: In what way are these message transfer methods incompatible?

When the dust has settled, it might be worth it to inquire about this upstream, maybe this points to a design change on their end, or there is a better way to achieve what we need?

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b0d4f85c..82d65bd4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,19 @@
 # CHANGELOG
 
+## [unreleased]
+
+_Use self defined objects instead of jsonrpc2-objects and jsonrpc2-pyclient._
+
+### Changed
+
+- Substitute `jsonrpc2-objects` and `jsonrpc2-pyclient` by self written objects.
+
+### Added
+
+- Add __future__.annotations to all files, which need it for annotations for Python 3.7/3.8.
+- Add self written `RPCServer` as alternative to openrpc package.
+
+
 ## [0.2.2] - 2024-02-14
 
 ### Fixed
diff --git a/environment.yml b/environment.yml
index 8e1c62d2..1c42d673 100644
--- a/environment.yml
+++ b/environment.yml
@@ -5,7 +5,6 @@ dependencies:
   - pyzmq=25.1.2
   - pip  # don't pin, to gain newest conda compatibility fixes
   - pip:
-    - jsonrpc2-pyclient==4.3.0
     - openrpc==8.1.0
     - uuid7==0.1.0
 # Development dependencies below
diff --git a/pyleco/actors/actor.py b/pyleco/actors/actor.py
index 62b2c9a1..c6f3d642 100644
--- a/pyleco/actors/actor.py
+++ b/pyleco/actors/actor.py
@@ -21,6 +21,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 # THE SOFTWARE.
 #
+from __future__ import annotations
 
 from typing import Any, Callable, Generic, Optional, Sequence, TypeVar, Union
 
diff --git a/pyleco/coordinators/coordinator.py b/pyleco/coordinators/coordinator.py
index f3d87317..88aa9e9c 100644
--- a/pyleco/coordinators/coordinator.py
+++ b/pyleco/coordinators/coordinator.py
@@ -22,13 +22,12 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from json import JSONDecodeError
 import logging
 from socket import gethostname
 from typing import Any, Optional, Union
 
-from jsonrpcobjects.objects import ErrorResponse, Request, ParamsRequest
-from openrpc import RPCServer
 import zmq
 
 if __name__ != "__main__":
@@ -38,6 +37,8 @@
     from ..core.serialization import get_json_content_type, JsonContentTypes
     from ..errors import CommunicationError
     from ..errors import NODE_UNKNOWN, RECEIVER_UNKNOWN, generate_error_with_data
+    from ..json_utils.json_objects import ErrorResponse, Request, ParamsRequest
+    from ..json_utils.rpc_server import RPCServer
     from ..utils.timers import RepeatingTimer
     from ..utils.zmq_log_handler import ZmqLogHandler
     from ..utils.events import Event, SimpleEvent
@@ -49,6 +50,8 @@
     from pyleco.core.serialization import get_json_content_type, JsonContentTypes
     from pyleco.errors import CommunicationError
     from pyleco.errors import NODE_UNKNOWN, RECEIVER_UNKNOWN, generate_error_with_data
+    from pyleco.json_utils.json_objects import ErrorResponse, Request, ParamsRequest
+    from pyleco.json_utils.rpc_server import RPCServer
     from pyleco.utils.timers import RepeatingTimer
     from pyleco.utils.zmq_log_handler import ZmqLogHandler
     from pyleco.utils.events import Event, SimpleEvent
@@ -102,13 +105,16 @@ def __init__(
         self.full_name = self.namespace + b".COORDINATOR"
         log.info(f"Start Coordinator of node {self.namespace!r} at port '{port}'.")
         self.address = f"{host or gethostname()}:{port}"
-        self.directory = Directory(namespace=self.namespace, full_name=self.full_name,
-                                   address=self.address)
+        self.directory = Directory(
+            namespace=self.namespace, full_name=self.full_name, address=self.address
+        )
         self.global_directory: dict[bytes, list[str]] = {}  # All Components
         self.timeout = timeout
-        self.cleaner = RepeatingTimer(interval=cleaning_interval,
-                                      function=self.remove_expired_addresses,
-                                      args=(expiration_time,))
+        self.cleaner = RepeatingTimer(
+            interval=cleaning_interval,
+            function=self.remove_expired_addresses,
+            args=(expiration_time,),
+        )
 
         self.cleaner.start()
 
@@ -164,13 +170,10 @@ def close(self) -> None:
             log.info(f"Coordinator {self.full_name!r} closed.")
             self.closed = True
 
-    def create_message(self, receiver: bytes, data: Optional[Union[bytes, str, object]] = None,
-                       **kwargs) -> Message:
-        return Message(
-            receiver=receiver,
-            sender=self.full_name,
-            data=data,
-            **kwargs)
+    def create_message(
+        self, receiver: bytes, data: Optional[Union[bytes, str, object]] = None, **kwargs
+    ) -> Message:
+        return Message(receiver=receiver, sender=self.full_name, data=data, **kwargs)
 
     def send_message(self, receiver: bytes, data: Optional[object] = None, **kwargs) -> None:
         """Send a message with any socket, including routing.
@@ -219,8 +222,9 @@ def _clean_components(self, expiration_time: float) -> None:
             self.sock.send_message(identity, message)
         self.publish_directory_update()
 
-    def routing(self, coordinators: Optional[list[str]] = None, stop_event: Optional[Event] = None
-                ) -> None:
+    def routing(
+        self, coordinators: Optional[list[str]] = None, stop_event: Optional[Event] = None
+    ) -> None:
         """Route all messages.
 
         Connect to Coordinators at the beginning.
@@ -230,8 +234,9 @@ def routing(self, coordinators: Optional[list[str]] = None, stop_event: Optional
         # Connect to Coordinators.
         if coordinators is not None:
             for coordinator in coordinators:
-                self.directory.add_node_sender(node=ZmqNode(context=self.context),
-                                               address=coordinator, namespace=b"")
+                self.directory.add_node_sender(
+                    node=ZmqNode(context=self.context), address=coordinator, namespace=b""
+                )
         # Route messages until told to stop.
         self.stop_event = stop_event or SimpleEvent()
         while not self.stop_event.is_set():
@@ -259,7 +264,8 @@ def deliver_message(self, sender_identity: bytes, message: Message) -> None:
         """
         log.debug(
             f"From identity {sender_identity!r}, from {message.sender!r}, to {message.receiver!r},"
-            f" header {message.header!r}, cid {message.conversation_id!r}, '{message.payload}'.")
+            f" header {message.header!r}, cid {message.conversation_id!r}, '{message.payload}'."
+        )
         # Update heartbeat
         if sender_identity:
             try:
@@ -323,14 +329,16 @@ def handle_commands(self, sender_identity: bytes, message: Message) -> None:
             self.handle_json_commands(message=message)
         else:
             log.error(
-                f"Message from {message.sender!r} of unknown type received: {message.payload[0]!r}")
+                f"Message from {message.sender!r} of unknown type received: {message.payload[0]!r}"
+            )
 
     def handle_json_commands(self, message: Message) -> None:
         try:
             data: Union[list[dict[str, Any]], dict[str, Any]] = message.data  # type: ignore
         except JSONDecodeError:
             log.error(
-                f"Invalid JSON message from {message.sender!r} received: {message.payload[0]!r}")
+                f"Invalid JSON message from {message.sender!r} received: {message.payload[0]!r}"
+            )
             return
         json_type = get_json_content_type(data)
         if JsonContentTypes.REQUEST in json_type:
@@ -339,7 +347,8 @@ def handle_json_commands(self, message: Message) -> None:
             except Exception as exc:
                 log.exception(
                     f"Invalid JSON-RPC message from {message.sender!r} received: {data}",
-                    exc_info=exc)
+                    exc_info=exc,
+                )
         elif JsonContentTypes.RESULT_RESPONSE == json_type:
             if data.get("result", False) is not None:  # type: ignore
                 log.info(f"Unexpeced result received: {data}")
@@ -351,7 +360,8 @@ def handle_json_commands(self, message: Message) -> None:
                     log.info(f"Unexpeced result received: {data}")
         else:
             log.error(
-                f"Invalid JSON RPC message from {message.sender!r} received: {message.payload[0]!r}")  # noqa
+                f"Invalid JSON RPC message from {message.sender!r} received: {message.payload[0]!r}"
+            )  # noqa
 
     def handle_rpc_call(self, message: Message) -> None:
         reply = self.rpc.process_request(message.payload[0])
@@ -430,12 +440,13 @@ def coordinator_sign_out(self) -> None:
             )
         )
 
-    def add_nodes(self, nodes: dict) -> None:  # : Dict[str, str]
+    def add_nodes(self, nodes: dict) -> None:  # : dict[str, str]
         for node, address in nodes.items():
             node = node.encode()
             try:
-                self.directory.add_node_sender(ZmqNode(context=self.context),
-                                               address=address, namespace=node)
+                self.directory.add_node_sender(
+                    ZmqNode(context=self.context), address=address, namespace=node
+                )
             except ValueError:
                 pass  # already connected
 
@@ -472,13 +483,12 @@ def publish_directory_update(self) -> None:
                 receiver=b".".join((node, b"COORDINATOR")),
                 message_type=MessageTypes.JSON,
                 data=[
+                    ParamsRequest(id=5, method="add_nodes", params={"nodes": nodes}).model_dump(),
                     ParamsRequest(
-                        id=5, method="add_nodes",
-                        params={"nodes": nodes}).model_dump(),
-                    ParamsRequest(
-                        id=6, method="record_components",
-                        params={"components": components}).model_dump(),
-                ])
+                        id=6, method="record_components", params={"components": components}
+                    ).model_dump(),
+                ],
+            )
 
 
 def main() -> None:
@@ -486,8 +496,12 @@ def main() -> None:
     from pyleco.utils.parser import parser, parse_command_line_parameters  # noqa: F811
 
     # Define parser
-    parser.add_argument("-c", "--coordinators", default="",
-                        help="connect to this comma separated list of coordinators")
+    parser.add_argument(
+        "-c",
+        "--coordinators",
+        default="",
+        help="connect to this comma separated list of coordinators",
+    )
     parser.add_argument("--namespace", help="set the Node's namespace")
     parser.add_argument("-p", "--port", type=int, help="port number to bind to")
 
@@ -496,7 +510,7 @@ def main() -> None:
     kwargs = parse_command_line_parameters(logger=gLog, parser=parser, logging_default=logging.INFO)
     if len(log.handlers) <= 1:
         log.addHandler(logging.StreamHandler())
-    cos = kwargs.pop('coordinators', "")
+    cos = kwargs.pop("coordinators", "")
     coordinators = cos.replace(" ", "").split(",")
 
     # Run the Coordinator
diff --git a/pyleco/core/data_message.py b/pyleco/core/data_message.py
index 12eaa76a..e61476c0 100644
--- a/pyleco/core/data_message.py
+++ b/pyleco/core/data_message.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from json import JSONDecodeError
 from typing import Any, Optional, Union
 
diff --git a/pyleco/core/internal_protocols.py b/pyleco/core/internal_protocols.py
index 369c97c4..60ce25e2 100644
--- a/pyleco/core/internal_protocols.py
+++ b/pyleco/core/internal_protocols.py
@@ -31,10 +31,11 @@
 For example a Director might use these tools to direct an Actor.
 """
 
+from __future__ import annotations
 from typing import Any, Optional, Protocol, Iterable, Union
 
 from .message import Message, MessageTypes
-from .rpc_generator import RPCGenerator
+from ..json_utils.rpc_generator import RPCGenerator
 
 
 class CommunicatorProtocol(Protocol):
diff --git a/pyleco/core/leco_protocols.py b/pyleco/core/leco_protocols.py
index 01f36620..865961a6 100644
--- a/pyleco/core/leco_protocols.py
+++ b/pyleco/core/leco_protocols.py
@@ -66,8 +66,9 @@ def test_method_is_available(component_methods, method):
         raise AssertionError(f"Method {method} is not available.")
 """
 
+from __future__ import annotations
 try:
-    from enum import StrEnum
+    from enum import StrEnum  # type: ignore
 except ImportError:
     # For python<3.11
     from enum import Enum
diff --git a/pyleco/core/message.py b/pyleco/core/message.py
index 8e7fef46..c50b8a76 100644
--- a/pyleco/core/message.py
+++ b/pyleco/core/message.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from json import JSONDecodeError
 from typing import Any, Optional, Union
 
diff --git a/pyleco/core/rpc_generator.py b/pyleco/core/rpc_generator.py
deleted file mode 100644
index b68f1943..00000000
--- a/pyleco/core/rpc_generator.py
+++ /dev/null
@@ -1,54 +0,0 @@
-#
-# This file is part of the PyLECO package.
-#
-# Copyright (c) 2023-2024 PyLECO Developers
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-
-from typing import Any, Union
-
-from jsonrpc2pyclient._irpcclient import IRPCClient  # type: ignore
-from jsonrpcobjects.objects import Error
-
-
-# according to error raised by IRPCClient if decoding fails.
-INVALID_SERVER_RESPONSE = Error(code=-32000, message="Invalid response from server.")
-
-
-class RPCGenerator(IRPCClient):
-    """Builds and interprets json rpc messages."""
-
-    # TODO it stores an always growing list of "id"s, if you do not call "get_result".
-
-    def build_request_str(self, method: str, *args, **kwargs) -> str:
-        if args and kwargs:
-            raise ValueError(
-                "You may not specify list of positional arguments "
-                "and give additional keyword arguments at the same time.")
-        return self._build_request(method=method, params=kwargs or list(args) or None
-                                   ).model_dump_json()
-
-    def get_result_from_response(self, data: Union[bytes, str]) -> Any:
-        """Get the result of that object or raise an error."""
-        return self._get_result_from_response(data=data)
-
-    def clear_id_list(self) -> None:
-        """Reset the list of created ids."""
-        self._ids: dict[int, int] = {}
diff --git a/pyleco/core/serialization.py b/pyleco/core/serialization.py
index 6760c02c..db043ecb 100644
--- a/pyleco/core/serialization.py
+++ b/pyleco/core/serialization.py
@@ -22,18 +22,20 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from enum import IntEnum, IntFlag
 import json
 from typing import Any, Optional, NamedTuple, Union
 
 from uuid_extensions import uuid7  # type: ignore  #  as long as uuid does not yet support UUIDv7
-from jsonrpcobjects.objects import (Request,
-                                    ParamsRequest,
-                                    ResultResponse,
-                                    ErrorResponse,
-                                    Notification,
-                                    ParamsNotification,
-                                    )
+from ..json_utils.json_objects import (
+    Request,
+    ParamsRequest,
+    ResultResponse,
+    ErrorResponse,
+    Notification,
+    ParamsNotification,
+)
 
 
 json_objects = (
diff --git a/pyleco/directors/coordinator_director.py b/pyleco/directors/coordinator_director.py
index 7dbd9f4d..572267d0 100644
--- a/pyleco/directors/coordinator_director.py
+++ b/pyleco/directors/coordinator_director.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 import logging
 
 from .director import Director
diff --git a/pyleco/directors/data_logger_director.py b/pyleco/directors/data_logger_director.py
index 08f6b505..59ab2d91 100644
--- a/pyleco/directors/data_logger_director.py
+++ b/pyleco/directors/data_logger_director.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 import logging
 from typing import Any, Optional
 
diff --git a/pyleco/directors/director.py b/pyleco/directors/director.py
index 0775673a..c2bda6b8 100644
--- a/pyleco/directors/director.py
+++ b/pyleco/directors/director.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 import logging
 from typing import Any, Optional, Sequence, Union
 
diff --git a/pyleco/directors/starter_director.py b/pyleco/directors/starter_director.py
index 7f42c5f3..68adb6e0 100644
--- a/pyleco/directors/starter_director.py
+++ b/pyleco/directors/starter_director.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 import logging
 from typing import Optional, Union
 
diff --git a/pyleco/directors/transparent_director.py b/pyleco/directors/transparent_director.py
index b44dd8ce..c91a0f60 100644
--- a/pyleco/directors/transparent_director.py
+++ b/pyleco/directors/transparent_director.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 import logging
 from typing import Generic, Optional, TypeVar, Union
 
diff --git a/pyleco/errors.py b/pyleco/errors.py
index f6fc824f..2c6d216f 100644
--- a/pyleco/errors.py
+++ b/pyleco/errors.py
@@ -24,7 +24,7 @@
 
 from typing import Any
 
-from jsonrpcobjects.objects import Error, DataError, ErrorResponse
+from .json_utils.json_objects import Error, DataError, ErrorResponse
 
 # JSON specification:
 # -32000 to -32099  Server error    reserved for implementation-defined server-errors
diff --git a/pyleco/json_utils/__init__.py b/pyleco/json_utils/__init__.py
new file mode 100644
index 00000000..c0acfaca
--- /dev/null
+++ b/pyleco/json_utils/__init__.py
@@ -0,0 +1,23 @@
+#
+# This file is part of the PyLECO package.
+#
+# Copyright (c) 2023-2024 PyLECO Developers
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
diff --git a/pyleco/json_utils/errors.py b/pyleco/json_utils/errors.py
new file mode 100644
index 00000000..a5161bd7
--- /dev/null
+++ b/pyleco/json_utils/errors.py
@@ -0,0 +1,104 @@
+"""This module provides exceptions for each JSON-RPC 2.0 error.
+
+There is one Exception defined for each pre-defined JSON-RPC 2.0 error.
+Additionally, there is a ServerError for implementation-defined errors.
+
+Each exception extends a base exception JSONRPCError.
+
+Copied from jsonrpc2-objects
+"""
+
+__all__ = (
+    "INTERNAL_ERROR",
+    "INVALID_PARAMS",
+    "INVALID_REQUEST",
+    "InternalError",
+    "InvalidParams",
+    "InvalidRequest",
+    "JSONRPCError",
+    "METHOD_NOT_FOUND",
+    "MethodNotFound",
+    "PARSE_ERROR",
+    "ParseError",
+    "ServerError",
+    "get_exception_by_code",
+)
+
+from typing import Optional, Type
+
+from .json_objects import DataError, Error, ErrorType
+
+SERVER_ERROR = Error(code=-32000, message="Server error")
+INVALID_REQUEST = Error(code=-32600, message="Invalid Request")
+METHOD_NOT_FOUND = Error(code=-32601, message="Method not found")
+INVALID_PARAMS = Error(code=-32602, message="Invalid params")
+INTERNAL_ERROR = Error(code=-32603, message="Internal error")
+PARSE_ERROR = Error(code=-32700, message="Parse error")
+
+
+class JSONRPCError(Exception):
+    """Base error that all JSON RPC exceptions extend."""
+
+    def __init__(self, error: ErrorType) -> None:
+        msg = f"{error.code}: {error.message}"
+        self.rpc_error = error
+        if isinstance(error, DataError):
+            msg += f"\nError Data: {error.data}"
+        super(JSONRPCError, self).__init__(msg)
+
+
+class ParseError(JSONRPCError):
+    """Error raised when invalid JSON was received by the server."""
+
+    def __init__(self, error: Optional[ErrorType] = None) -> None:
+        super(ParseError, self).__init__(error or PARSE_ERROR)
+
+
+class InvalidRequest(JSONRPCError):
+    """Error raised when the JSON sent is not a valid Request object."""
+
+    def __init__(self, error: Optional[ErrorType] = None) -> None:
+        super(InvalidRequest, self).__init__(error or INVALID_REQUEST)
+
+
+class MethodNotFound(JSONRPCError):
+    """Error raised when the method does not exist / is not available."""
+
+    def __init__(self, error: Optional[ErrorType] = None) -> None:
+        super(MethodNotFound, self).__init__(error or METHOD_NOT_FOUND)
+
+
+class InvalidParams(JSONRPCError):
+    """Error raised when invalid method parameter(s) are supplied."""
+
+    def __init__(self, error: Optional[ErrorType] = None) -> None:
+        super(InvalidParams, self).__init__(error or INVALID_PARAMS)
+
+
+class InternalError(JSONRPCError):
+    """Error raised when there is an internal JSON-RPC error."""
+
+    def __init__(self, error: Optional[ErrorType] = None) -> None:
+        super(InternalError, self).__init__(error or INTERNAL_ERROR)
+
+
+class ServerError(JSONRPCError):
+    """Error raised when a server error occurs."""
+
+    def __init__(self, error: ErrorType) -> None:
+        super(ServerError, self).__init__(error)
+
+
+def get_exception_by_code(code: int) -> Optional[Type[JSONRPCError]]:
+    """Get the JSON-RPC error corresponding to an error code.
+
+    :param code: The JSON-RPC error code.
+    :return: JSON RPC error object or None.
+    """
+    return {
+        -32600: InvalidRequest,
+        -32601: MethodNotFound,
+        -32602: InvalidParams,
+        -32603: InternalError,
+        -32700: ParseError,
+    }.get(code)
diff --git a/pyleco/json_utils/json_objects.py b/pyleco/json_utils/json_objects.py
new file mode 100644
index 00000000..bd02ea83
--- /dev/null
+++ b/pyleco/json_utils/json_objects.py
@@ -0,0 +1,128 @@
+#
+# This file is part of the PyLECO package.
+#
+# Copyright (c) 2023-2024 PyLECO Developers
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+"""
+Names based on the classes of jsonrpc2-objects.
+
+As jsonrpc2-objects uses pydantic models, these objects offer the `dump_model` and `dump_model_json`
+methods.
+"""
+
+from __future__ import annotations
+from dataclasses import asdict, dataclass
+import json
+from typing import Any, Optional, Union
+
+
+ErrorType = Union["DataError", "Error"]
+NotificationType = Union["Notification", "ParamsNotification"]
+RequestType = Union["ParamsRequest", "Request"]
+ResponseType = Union["ErrorResponse", "ResultResponse"]
+
+
+@dataclass
+class JsonObject:
+    def model_dump(self) -> dict[str, Any]:
+        """Create a dictionary of the attributes."""
+        return asdict(self)
+
+    def model_dump_json(self) -> str:
+        """Create a json representation."""
+        return json.dumps(self.model_dump(), separators=(",", ":"))
+
+
+@dataclass
+class Request(JsonObject):
+    """Request the result of a remote call."""
+
+    id: Union[int, str]
+    method: str
+    jsonrpc: str = "2.0"
+
+
+@dataclass
+class ParamsRequest(JsonObject):
+    """Request the result of a remote call with parameters."""
+
+    id: Union[int, str]
+    method: str
+    params: Union[list, dict]
+    jsonrpc: str = "2.0"
+
+
+@dataclass
+class Notification(JsonObject):
+    """Do a remote call without requesting a response."""
+
+    method: str
+    jsonrpc: str = "2.0"
+
+
+@dataclass
+class ParamsNotification(JsonObject):
+    """Do a remote call with parameters without requesting a response."""
+
+    method: str
+    params: Union[list, dict]
+    jsonrpc: str = "2.0"
+
+
+@dataclass
+class ResultResponse(JsonObject):
+    """A response containing a result."""
+
+    id: Union[int, str]
+    result: Any
+    jsonrpc: str = "2.0"
+
+
+@dataclass
+class Error(JsonObject):
+    """An error to be sent via an :class:`ErrorResponse`."""
+
+    code: int
+    message: str
+
+
+@dataclass
+class DataError(JsonObject):
+    """An error with data, to be sent via an :class:`ErrorResponse`."""
+
+    code: int
+    message: str
+    data: Any
+
+
+@dataclass
+class ErrorResponse(JsonObject):
+    """A response containing an error."""
+
+    id: Optional[Union[int, str]]
+    error: ErrorType
+    jsonrpc: str = "2.0"
+
+    def model_dump(self) -> dict[str, Any]:
+        pre_dict = asdict(self)
+        pre_dict["error"] = asdict(self.error)
+        return pre_dict
diff --git a/pyleco/json_utils/rpc_generator.py b/pyleco/json_utils/rpc_generator.py
new file mode 100644
index 00000000..28f67111
--- /dev/null
+++ b/pyleco/json_utils/rpc_generator.py
@@ -0,0 +1,100 @@
+#
+# This file is part of the PyLECO package.
+#
+# Copyright (c) 2023-2024 PyLECO Developers
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+from __future__ import annotations
+import json
+import logging
+from typing import Any, Union
+
+from .json_objects import Request, ParamsRequest, DataError, Error, ResultResponse
+from .errors import ServerError, get_exception_by_code, JSONRPCError
+
+log = logging.getLogger(__name__)
+log.addHandler(logging.NullHandler())
+
+
+INVALID_SERVER_RESPONSE = Error(code=-32000, message="Invalid response from server.")
+
+
+class RPCGenerator:
+    """This class can generate a JSONRPC request string and interpret the result string."""
+
+    _id_counter: int = 1
+
+    def build_request_str(self, method: str, *args, **kwargs) -> str:
+        if args and kwargs:
+            raise ValueError(
+                "You may not specify list of positional arguments "
+                "and give additional keyword arguments at the same time."
+            )
+        id = self._id_counter
+        self._id_counter += 1
+        r: Union[Request, ParamsRequest]
+        if args or kwargs:
+            r = ParamsRequest(id=id, method=method, params=kwargs or list(args))
+        else:
+            r = Request(id=id, method=method)
+        return r.model_dump_json()
+
+    def get_result_from_response(self, data: Union[bytes, str, dict]) -> Any:
+        """Get the result of that object or raise an error."""
+        # copied and modified from jsonrpc2-pyclient
+        try:
+            # Parse string to JSON.
+            if not isinstance(data, dict):
+                json_data = json.loads(data)
+            else:
+                json_data = data
+
+            # Raise error if JSON RPC error response.
+            if error_content := json_data.get("error"):
+                error: Union[Error, DataError]
+                if error_content.get("data"):
+                    error = DataError(**error_content)
+                else:
+                    error = Error(**error_content)
+                exc = get_exception_by_code(error.code) or ServerError
+                raise exc(error)
+
+            # Return result if JSON RPC result response.
+            if "result" in json_data.keys():
+                return ResultResponse(**json_data).result
+
+            # Not valid JSON RPC response if it has no error or result.
+            raise JSONRPCError(
+                DataError(
+                    code=INVALID_SERVER_RESPONSE.code,
+                    message=INVALID_SERVER_RESPONSE.message,
+                    data=json_data,
+                )
+            )
+        except (json.JSONDecodeError, TypeError, AttributeError) as exc:
+            log.exception(f"{type(exc).__name__}:")
+            raise JSONRPCError(
+                DataError(
+                    code=INVALID_SERVER_RESPONSE.code,
+                    message=INVALID_SERVER_RESPONSE.message,
+                    data=data,
+                )
+            ) from exc
diff --git a/pyleco/json_utils/rpc_server.py b/pyleco/json_utils/rpc_server.py
new file mode 100644
index 00000000..b1e83e62
--- /dev/null
+++ b/pyleco/json_utils/rpc_server.py
@@ -0,0 +1,28 @@
+#
+# This file is part of the PyLECO package.
+#
+# Copyright (c) 2023-2024 PyLECO Developers
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+try:
+    from openrpc import RPCServer  # type: ignore  # noqa: F401
+except ModuleNotFoundError:
+    from .rpc_server_definition import RPCServer  # type: ignore  # noqa: F401
diff --git a/pyleco/json_utils/rpc_server_definition.py b/pyleco/json_utils/rpc_server_definition.py
new file mode 100644
index 00000000..8d2273f3
--- /dev/null
+++ b/pyleco/json_utils/rpc_server_definition.py
@@ -0,0 +1,140 @@
+#
+# This file is part of the PyLECO package.
+#
+# Copyright (c) 2023-2024 PyLECO Developers
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+from __future__ import annotations
+import json
+import logging
+from typing import Any, Callable, Optional, Union
+
+from .errors import INTERNAL_ERROR, SERVER_ERROR, INVALID_REQUEST
+from .json_objects import ResultResponse, ErrorResponse
+from ..errors import generate_error_with_data
+
+
+log = logging.getLogger(__name__)
+log.addHandler(logging.NullHandler())
+
+
+class RPCServer:
+    def __init__(
+        self,
+        title: Optional[str] = None,
+        version: Optional[str] = None,
+        debug: bool = False,
+        **kwargs,
+    ) -> None:
+        super().__init__(**kwargs)
+        self.title = title or "RPC Server"
+        self._version = version or "0.1.0"
+        self._rpc_methods: dict[str, Callable] = {}
+        self.method(name="rpc.discover")(self.discover)
+
+    def method(self, name: Optional[str] = None, **kwargs) -> Callable[[Callable], None]:
+        if name is None:
+
+            def method_registrar(method: Callable) -> None:
+                return self._register_method(name=method.__name__, method=method)
+        else:
+
+            def method_registrar(method: Callable) -> None:
+                return self._register_method(name=name, method=method)
+
+        return method_registrar
+
+    def _register_method(self, name: str, method: Callable) -> None:
+        self._rpc_methods[name] = method
+
+    def process_request(self, data: Union[bytes, str]) -> Optional[str]:
+        try:
+            json_data = json.loads(data)
+            if isinstance(json_data, list):
+                results = []
+                for element in json_data:
+                    result = self._process_single_request(element)
+                    if result is not None:
+                        results.append(result.model_dump())
+                if results:
+                    return json.dumps(results, separators=(",", ":"))
+                else:
+                    return None
+            elif isinstance(json_data, dict):
+                result = self._process_single_request(json_data)
+                if result:
+                    return result.model_dump_json()
+                else:
+                    return None
+            else:
+                return ErrorResponse(
+                    id=None,
+                    error=generate_error_with_data(INVALID_REQUEST, json_data),
+                ).model_dump_json()
+        except Exception as exc:
+            log.exception(f"{type(exc).__name__}:", exc_info=exc)
+            return ErrorResponse(id=None, error=INTERNAL_ERROR).model_dump_json()
+
+    def _process_single_request(
+        self, request: dict[str, Any]
+    ) -> Union[ResultResponse, ErrorResponse, None]:
+        id_ = None
+        try:
+            id_ = request.get("id")
+            method_name = request.get("method")
+            if method_name is None:
+                return ErrorResponse(
+                    id=id_, error=generate_error_with_data(INVALID_REQUEST, data=request)
+                )
+            params = request.get("params")
+            method = self._rpc_methods[method_name]
+            if isinstance(params, dict):
+                result = method(**params)
+            elif isinstance(
+                params,
+                list,
+            ):
+                result = method(*params)
+            else:
+                result = method()
+            if id_ is not None:
+                return ResultResponse(id=id_, result=result)
+            else:
+                return None
+        except Exception as exc:
+            log.exception(f"{type(exc).__name__}:", exc_info=exc)
+            return ErrorResponse(id=id_, error=SERVER_ERROR)
+
+    def discover(self) -> dict[str, Any]:
+        """list all the capabilities of the server."""
+        result: dict[str, Any] = {"openrpc": "1.2.6"}
+        result["info"] = {"title": self.title, "version": self._version}
+        methods: list[dict[str, Any]] = []
+        for name, method in self._rpc_methods.items():
+            if name == "rpc.discover":
+                # do not list it
+                continue
+            method_dict = {"name": name}
+            if method.__doc__:
+                method_dict["description"] = method.__doc__
+            methods.append(method_dict)
+        result["methods"] = methods
+        return result
diff --git a/pyleco/management/data_logger.py b/pyleco/management/data_logger.py
index b1e52a13..5eaf7cb8 100644
--- a/pyleco/management/data_logger.py
+++ b/pyleco/management/data_logger.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from typing import Union, Sequence
 
 import datetime
diff --git a/pyleco/management/starter.py b/pyleco/management/starter.py
index 1f06632c..e3fa6461 100644
--- a/pyleco/management/starter.py
+++ b/pyleco/management/starter.py
@@ -34,6 +34,7 @@
 Created on Thu Dec 15 09:31:04 2022 by Benedikt Moneke
 """
 
+from __future__ import annotations
 from enum import IntFlag
 from importlib import import_module, reload
 import logging
diff --git a/pyleco/utils/base_communicator.py b/pyleco/utils/base_communicator.py
index d2d1cc04..4e03680f 100644
--- a/pyleco/utils/base_communicator.py
+++ b/pyleco/utils/base_communicator.py
@@ -22,16 +22,17 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 import logging
 from time import perf_counter
 from typing import Optional, Protocol
 
-from jsonrpcobjects.errors import JSONRPCError
 import zmq
 
 from ..core.internal_protocols import CommunicatorProtocol
 from ..core.message import Message, MessageTypes
 from ..errors import DUPLICATE_NAME, NOT_SIGNED_IN
+from ..json_utils.errors import JSONRPCError
 
 
 NOT_SIGNED_IN_ERROR_CODE = str(NOT_SIGNED_IN.code).encode()
diff --git a/pyleco/utils/communicator.py b/pyleco/utils/communicator.py
index 1db4f280..0f78c0dc 100644
--- a/pyleco/utils/communicator.py
+++ b/pyleco/utils/communicator.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 import logging
 from time import perf_counter
 from typing import Optional, Union
@@ -31,7 +32,7 @@
 from ..errors import NOT_SIGNED_IN
 from ..core import COORDINATOR_PORT
 from ..core.message import Message, MessageTypes
-from ..core.rpc_generator import RPCGenerator
+from ..json_utils.rpc_generator import RPCGenerator
 from .base_communicator import BaseCommunicator
 
 
diff --git a/pyleco/utils/coordinator_utils.py b/pyleco/utils/coordinator_utils.py
index d3e27c52..8bdfeb39 100644
--- a/pyleco/utils/coordinator_utils.py
+++ b/pyleco/utils/coordinator_utils.py
@@ -22,20 +22,21 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from abc import abstractmethod
 from dataclasses import dataclass
 import logging
 from time import perf_counter
 from typing import Protocol, Optional, Union
 
-from jsonrpcobjects.objects import ErrorResponse, Request
 import zmq
 
 from ..core import COORDINATOR_PORT
 from ..errors import CommunicationError, NOT_SIGNED_IN, DUPLICATE_NAME
 from ..core.message import Message, MessageTypes
 from ..core.serialization import deserialize_data
-from ..core.rpc_generator import RPCGenerator
+from ..json_utils.rpc_generator import RPCGenerator
+from ..json_utils.json_objects import ErrorResponse, Request
 
 
 log = logging.getLogger(__name__)
diff --git a/pyleco/utils/data_publisher.py b/pyleco/utils/data_publisher.py
index 130ab125..46add4ca 100644
--- a/pyleco/utils/data_publisher.py
+++ b/pyleco/utils/data_publisher.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 import logging
 import pickle
 from typing import Any, Optional, Union
diff --git a/pyleco/utils/extended_message_handler.py b/pyleco/utils/extended_message_handler.py
index def09617..02817d16 100644
--- a/pyleco/utils/extended_message_handler.py
+++ b/pyleco/utils/extended_message_handler.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 import json
 import pickle
 from typing import Optional
diff --git a/pyleco/utils/message_handler.py b/pyleco/utils/message_handler.py
index c577734a..60f58601 100644
--- a/pyleco/utils/message_handler.py
+++ b/pyleco/utils/message_handler.py
@@ -22,20 +22,21 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from json import JSONDecodeError
 import logging
 import time
 from typing import Any, Callable, Optional, Union
 
-from jsonrpcobjects.errors import JSONRPCError
-from openrpc import RPCServer
 import zmq
 
 from ..core import COORDINATOR_PORT
 from ..core.leco_protocols import ExtendedComponentProtocol
 from ..core.message import Message, MessageTypes
-from ..core.rpc_generator import RPCGenerator
 from ..core.serialization import JsonContentTypes, get_json_content_type
+from ..json_utils.errors import JSONRPCError
+from ..json_utils.rpc_generator import RPCGenerator
+from ..json_utils.rpc_server import RPCServer
 from .base_communicator import BaseCommunicator
 from .log_levels import PythonLogLevels
 from .zmq_log_handler import ZmqLogHandler
@@ -62,13 +63,19 @@ class MessageHandler(BaseCommunicator, ExtendedComponentProtocol):
     :param protocol: Connection protocol.
     :param log: Logger instance whose logs should be published. Defaults to `getLogger("__main__")`.
     """
+
     name: str
 
-    def __init__(self, name: str,
-                 host: str = "localhost", port: int = COORDINATOR_PORT, protocol: str = "tcp",
-                 log: Optional[logging.Logger] = None,
-                 context: Optional[zmq.Context] = None,
-                 **kwargs):
+    def __init__(
+        self,
+        name: str,
+        host: str = "localhost",
+        port: int = COORDINATOR_PORT,
+        protocol: str = "tcp",
+        log: Optional[logging.Logger] = None,
+        context: Optional[zmq.Context] = None,
+        **kwargs,
+    ):
         self.name = name
         self._namespace: Union[str, None] = None
         self._full_name: str = name
@@ -79,8 +86,9 @@ def __init__(self, name: str,
         self.register_rpc_methods()
 
         self.setup_logging(log=log)
-        self.setup_socket(host=host, port=port, protocol=protocol,
-                          context=context or zmq.Context.instance())
+        self.setup_socket(
+            host=host, port=port, protocol=protocol, context=context or zmq.Context.instance()
+        )
 
         super().__init__(**kwargs)
 
@@ -135,11 +143,13 @@ def register_rpc_methods(self) -> None:
         self.register_rpc_method(self.pong)
 
     # Base communication
-    def send(self,
-             receiver: Union[bytes, str],
-             conversation_id: Optional[bytes] = None,
-             data: Optional[Any] = None,
-             **kwargs) -> None:
+    def send(
+        self,
+        receiver: Union[bytes, str],
+        conversation_id: Optional[bytes] = None,
+        data: Optional[Any] = None,
+        **kwargs,
+    ) -> None:
         """Send a message to a receiver with serializable `data`."""
         try:
             super().send(receiver=receiver, conversation_id=conversation_id, data=data, **kwargs)
@@ -181,8 +191,9 @@ def _listen_setup(self) -> zmq.Poller:
         self.next_beat = time.perf_counter() + heartbeat_interval
         return poller
 
-    def _listen_loop_element(self, poller: zmq.Poller, waiting_time: Optional[int]
-                             ) -> dict[zmq.Socket, int]:
+    def _listen_loop_element(
+        self, poller: zmq.Poller, waiting_time: Optional[int]
+    ) -> dict[zmq.Socket, int]:
         """Check the socks for incoming messages and handle them.
 
         :param waiting_time: Timeout of the poller in ms.
@@ -203,8 +214,7 @@ def _listen_close(self, waiting_time: Optional[int] = None) -> None:
 
     # Message handling in loop
     def read_and_handle_message(self) -> None:
-        """Interpret incoming message, which have not been requested.
-        """
+        """Interpret incoming message, which have not been requested."""
         try:
             message = self.read_message(timeout=0)
         except (TimeoutError, JSONRPCError):
@@ -224,7 +234,7 @@ def handle_message(self, message: Message) -> None:
     def handle_json_message(self, message: Message) -> None:
         try:
             data: dict[str, Any] = message.data  # type: ignore
-        except (JSONDecodeError) as exc:
+        except JSONDecodeError as exc:
             self.log.exception(f"Could not decode json message {message}", exc_info=exc)
             return
         content = get_json_content_type(data)
@@ -244,8 +254,12 @@ def handle_json_request(self, message: Message) -> None:
     def process_json_message(self, message: Message) -> Message:
         self.log.info(f"Handling commands of {message}.")
         reply = self.rpc.process_request(message.payload[0])
-        response = Message(message.sender, conversation_id=message.conversation_id,
-                           message_type=MessageTypes.JSON, data=reply)
+        response = Message(
+            message.sender,
+            conversation_id=message.conversation_id,
+            message_type=MessageTypes.JSON,
+            data=reply,
+        )
         return response
 
     def handle_json_error(self, message: Message) -> None:
@@ -256,9 +270,10 @@ def handle_json_result(self, message: Message) -> None:
 
     def handle_unknown_message_type(self, message: Message) -> None:
         self.log.warning(
-                f"Message from {message.sender!r} with unknown message type "
-                f"{message.header_elements.message_type} received: '{message.data}', "
-                f"{message.payload!r}.")
+            f"Message from {message.sender!r} with unknown message type "
+            f"{message.header_elements.message_type} received: '{message.data}', "
+            f"{message.payload!r}."
+        )
 
     # Methods offered via RPC
     def set_log_level(self, level: str) -> None:
diff --git a/pyleco/utils/parser.py b/pyleco/utils/parser.py
index 3667636d..c45e1a6e 100644
--- a/pyleco/utils/parser.py
+++ b/pyleco/utils/parser.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from argparse import ArgumentParser
 import logging
 from typing import Optional
diff --git a/pyleco/utils/pipe_handler.py b/pyleco/utils/pipe_handler.py
index bed6cd2d..c6c12a35 100644
--- a/pyleco/utils/pipe_handler.py
+++ b/pyleco/utils/pipe_handler.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from enum import Enum
 from threading import get_ident, Condition
 from typing import Any, Callable, Optional, Union
@@ -314,7 +315,7 @@ def _send_frames(self, frames: list[bytes]) -> None:
         self.socket.send_multipart(frames)
 
     def read_message(self, conversation_id: Optional[bytes] = None,
-                          timeout: Optional[float] = None) -> Message:
+                     timeout: Optional[float] = None) -> Message:
         """Read a message using the thread safe buffer."""
         message = self._read_socket_message(timeout=timeout)
         self.check_for_not_signed_in_error(message=message)
diff --git a/pyleco/utils/zmq_log_handler.py b/pyleco/utils/zmq_log_handler.py
index 7c671c1a..a5db4c3c 100644
--- a/pyleco/utils/zmq_log_handler.py
+++ b/pyleco/utils/zmq_log_handler.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 import logging
 from logging.handlers import QueueHandler
 import time
diff --git a/pyproject.toml b/pyproject.toml
index b08f58e1..ee84c633 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,9 +21,7 @@ classifiers = [
 requires-python = ">=3.9"
 dependencies = [
   "pyzmq >= 22.3.0",
-  "openrpc >= 8.1.0",
-  "jsonrpc2-pyclient >= 4.3.0",
-  "jsonrpc2-objects >= 3.0.0",
+  "openrpc >= 8.1.0; python_version >= '3.9'",
   "uuid7 >= 0.1.0",
 ]
 
@@ -81,4 +79,6 @@ omit = [
     # Omit LECO definitions
     "pyleco/errors.py",
     "pyleco/core/leco_protocols.py",
+    # exclude import file
+    "pyleco/json_utils/rpc_server.py",
 ]


Test patch:
diff --git a/pyleco/test.py b/pyleco/test.py
index ce975db6..8d1da5fd 100644
--- a/pyleco/test.py
+++ b/pyleco/test.py
@@ -22,11 +22,12 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from typing import Any, Optional, Sequence, Union
 
 from .core.message import Message
 from .core.internal_protocols import CommunicatorProtocol
-from .core.rpc_generator import RPCGenerator
+from .json_utils.rpc_generator import RPCGenerator
 
 
 class FakeContext:
diff --git a/tests/acceptance_tests/test_proxy_server_live.py b/tests/acceptance_tests/test_proxy_server_live.py
index 93e9effe..e0c7cafe 100644
--- a/tests/acceptance_tests/test_proxy_server_live.py
+++ b/tests/acceptance_tests/test_proxy_server_live.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 import pytest
 from time import sleep
 
diff --git a/tests/coordinators/test_coordinator.py b/tests/coordinators/test_coordinator.py
index b76d1728..66aff32e 100644
--- a/tests/coordinators/test_coordinator.py
+++ b/tests/coordinators/test_coordinator.py
@@ -22,18 +22,19 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from unittest.mock import MagicMock
 
 import pytest
-from jsonrpcobjects.objects import Request, ErrorResponse
 
+from pyleco.json_utils.json_objects import Request, ErrorResponse
 from pyleco.errors import (NODE_UNKNOWN, NOT_SIGNED_IN, DUPLICATE_NAME, RECEIVER_UNKNOWN,
                            generate_error_with_data)
 from pyleco.core import VERSION_B
 from pyleco.core.message import Message, MessageTypes
 from pyleco.core.leco_protocols import ExtendedComponentProtocol, Protocol, CoordinatorProtocol
 from pyleco.utils.coordinator_utils import FakeMultiSocket, FakeNode
-from pyleco.core.rpc_generator import RPCGenerator
+from pyleco.json_utils.rpc_generator import RPCGenerator
 from pyleco.test import FakeContext
 from pyleco.utils.events import SimpleEvent
 
diff --git a/tests/core/test_internal_protocols.py b/tests/core/test_internal_protocols.py
index 53038b75..7a345954 100644
--- a/tests/core/test_internal_protocols.py
+++ b/tests/core/test_internal_protocols.py
@@ -24,11 +24,10 @@
 
 import pytest
 
-from jsonrpcobjects.errors import JSONRPCError
-
 from pyleco.core.message import Message, MessageTypes
 from pyleco.core.internal_protocols import CommunicatorProtocol
 from pyleco.test import FakeCommunicator
+from pyleco.json_utils.errors import JSONRPCError
 
 cid = b"conversation_id;"
 
diff --git a/tests/core/test_serialization.py b/tests/core/test_serialization.py
index 0d3f9615..bb950841 100644
--- a/tests/core/test_serialization.py
+++ b/tests/core/test_serialization.py
@@ -22,10 +22,11 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from typing import Any, Optional, Union
 
 import pytest
-from jsonrpcobjects.objects import Request
+from pyleco.json_utils.json_objects import Request
 
 from pyleco.core import serialization
 from pyleco.core.serialization import JsonContentTypes, get_json_content_type
diff --git a/tests/json_utils/test_json_objects.py b/tests/json_utils/test_json_objects.py
new file mode 100644
index 00000000..5df37ca5
--- /dev/null
+++ b/tests/json_utils/test_json_objects.py
@@ -0,0 +1,77 @@
+#
+# This file is part of the PyLECO package.
+#
+# Copyright (c) 2023-2024 PyLECO Developers
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+from pyleco.json_utils import json_objects
+
+
+def test_request():
+    request = json_objects.Request(id=5, method="call")
+    assert request.model_dump() == {
+        "id": 5,
+        "jsonrpc": "2.0",
+        "method": "call",
+    }
+
+
+def test_request_with_params():
+    request = json_objects.ParamsRequest(id=5, method="call", params=[1, 5])
+    assert request.model_dump() == {
+        "id": 5,
+        "jsonrpc": "2.0",
+        "method": "call",
+        "params": [1, 5],
+    }
+
+
+def test_notification():
+    request = json_objects.Notification(method="call")
+    assert request.model_dump() == {
+        "jsonrpc": "2.0",
+        "method": "call",
+    }
+
+
+def test_notification_with_params():
+    request = json_objects.ParamsNotification(method="call", params=[1, 5])
+    assert request.model_dump() == {
+        "jsonrpc": "2.0",
+        "method": "call",
+        "params": [1, 5],
+    }
+
+
+def test_result():
+    result = json_objects.ResultResponse(id=5, result=7)
+    assert result.model_dump() == {
+        "id": 5,
+        "jsonrpc": "2.0",
+        "result": 7,
+    }
+
+
+def test_error_with_data():
+    """Test that the Error object is json serializable."""
+    data_error = json_objects.DataError(code=5, message="whatever", data="abc")
+    error_response = json_objects.ErrorResponse(id=7, error=data_error)
+    assert error_response.model_dump_json() == '{"id":7,"error":{"code":5,"message":"whatever","data":"abc"},"jsonrpc":"2.0"}'  # noqa
diff --git a/tests/core/test_rpc_generator.py b/tests/json_utils/test_rpc_generator.py
similarity index 91%
rename from tests/core/test_rpc_generator.py
rename to tests/json_utils/test_rpc_generator.py
index cf095bc5..fa9aff1a 100644
--- a/tests/core/test_rpc_generator.py
+++ b/tests/json_utils/test_rpc_generator.py
@@ -26,11 +26,11 @@
 
 import pytest
 
-from jsonrpcobjects.objects import ErrorResponse
-from jsonrpcobjects.errors import JSONRPCError
+from pyleco.json_utils.json_objects import ErrorResponse
+from pyleco.json_utils.errors import JSONRPCError
 
 from pyleco.errors import NODE_UNKNOWN, NOT_SIGNED_IN, DUPLICATE_NAME, RECEIVER_UNKNOWN
-from pyleco.core.rpc_generator import RPCGenerator, INVALID_SERVER_RESPONSE
+from pyleco.json_utils.rpc_generator import RPCGenerator, INVALID_SERVER_RESPONSE
 
 
 @pytest.fixture
@@ -55,12 +55,6 @@ def test_build_request_str_raises_error(generator: RPCGenerator):
         generator.build_request_str("some_method", "argument", keyword="whatever")
 
 
-def test_clear_ids(generator: RPCGenerator):
-    generator._ids = {7: 7}
-    generator.clear_id_list()
-    assert generator._ids == {}
-
-
 @pytest.mark.parametrize("response, result", (
         ('{"id": 5, "result": 7.9, "jsonrpc": "2.0"}', 7.9),
         (b'{"id": 5, "result": 7.9, "jsonrpc": "2.0"}', 7.9),  # bytes version
diff --git a/tests/json_utils/test_rpc_server.py b/tests/json_utils/test_rpc_server.py
new file mode 100644
index 00000000..271f5c87
--- /dev/null
+++ b/tests/json_utils/test_rpc_server.py
@@ -0,0 +1,171 @@
+#
+# This file is part of the PyLECO package.
+#
+# Copyright (c) 2023-2024 PyLECO Developers
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+
+import json
+
+import pytest
+
+from pyleco.json_utils.rpc_generator import RPCGenerator
+from pyleco.json_utils.rpc_server_definition import RPCServer
+from pyleco.json_utils.json_objects import Request, ParamsRequest, ResultResponse
+from pyleco.json_utils.errors import ServerError, InvalidRequest, INVALID_REQUEST, SERVER_ERROR
+
+try:
+    # Load openrpc server for comparison, if available.
+    from openrpc import RPCServer as RPCServerOpen
+except ModuleNotFoundError:
+    rpc_server_classes: list = [RPCServer]
+else:
+    rpc_server_classes = [RPCServer, RPCServerOpen]
+
+
+@pytest.fixture
+def rpc_generator() -> RPCGenerator:
+    return RPCGenerator()
+
+
+def side_effect_method(arg=None):
+    global args
+    args = (arg,)
+    return 5
+
+
+def fail():
+    """Fails always."""
+    raise NotImplementedError
+
+
+def simple():
+    """A method without parameters."""
+    return 7
+
+
+def obligatory_parameter(arg1: float):
+    """Needs an argument"""
+    return arg1 * 2
+
+
+@pytest.fixture(params=rpc_server_classes)
+def rpc_server(request) -> RPCServer:
+    rpc_server = request.param()
+    rpc_server.method(name="sem")(side_effect_method)
+    rpc_server.method()(side_effect_method)
+    rpc_server.method()(fail)
+    rpc_server.method()(simple)
+    rpc_server.method()(obligatory_parameter)
+    return rpc_server
+
+
+def test_success(rpc_generator: RPCGenerator, rpc_server: RPCServer):
+    request = rpc_generator.build_request_str(method="sem", arg=3)
+    response = rpc_server.process_request(request)
+    result = rpc_generator.get_result_from_response(response)  # type: ignore
+    assert result == 5
+    assert args == (3,)  # type: ignore
+
+
+def test_multiple_requests_success(rpc_server: RPCServer, rpc_generator: RPCGenerator):
+    request1 = ParamsRequest(id=1, method="sem", params=[3])
+    request2 = Request(id=2, method="side_effect_method")
+    message = json.dumps([request1.model_dump(), request2.model_dump()])
+    result = rpc_server.process_request(message)
+    result_obj = json.loads(result)  # type: ignore
+    assert rpc_generator.get_result_from_response(result_obj[0]) == 5
+    assert rpc_generator.get_result_from_response(result_obj[1]) == 5
+
+
+def test_failing_method(rpc_generator: RPCGenerator, rpc_server: RPCServer):
+    request = rpc_generator.build_request_str(method="fail")
+    response = rpc_server.process_request(request)
+    with pytest.raises(ServerError) as exc_info:
+        rpc_generator.get_result_from_response(response)  # type: ignore
+    error = exc_info.value.rpc_error
+    assert error.code == SERVER_ERROR.code
+    assert error.message == SERVER_ERROR.message
+
+
+@pytest.mark.xfail(True, reason="Self written RPCServer cannot handle additional args")
+def test_wrong_method_arguments(rpc_generator: RPCGenerator, rpc_server: RPCServer):
+    request = rpc_generator.build_request_str(method="simple", arg=9)
+    response = rpc_server.process_request(request)
+    result = rpc_generator.get_result_from_response(response)  # type: ignore
+    assert result == 7
+
+
+def test_obligatory_parameter_missing(rpc_generator: RPCGenerator, rpc_server: RPCServer):
+    request = rpc_generator.build_request_str(method="obligatory_parameter")
+    response = rpc_server.process_request(request)
+    with pytest.raises(ServerError) as exc_info:
+        rpc_generator.get_result_from_response(response)  # type: ignore
+    error = exc_info.value.rpc_error
+    assert error.code == SERVER_ERROR.code
+    assert error.message == SERVER_ERROR.message
+
+
+def test_process_response(rpc_server: RPCServer, rpc_generator: RPCGenerator):
+    """It should be a request, not a response!"""
+    request = ResultResponse(id=7, result=9)
+    request_string = request.model_dump_json()
+    response = rpc_server.process_request(request_string)
+    with pytest.raises(InvalidRequest) as exc_info:
+        rpc_generator.get_result_from_response(response)  # type: ignore
+    error = exc_info.value.rpc_error
+    assert error.code == INVALID_REQUEST.code
+    assert error.message == INVALID_REQUEST.message
+    # assert error.data == request.model_dump()  # type: ignore
+
+
+class Test_discover_method:
+
+    @pytest.fixture
+    def discovered(self, rpc_server: RPCServer, rpc_generator: RPCGenerator) -> dict:
+        request = rpc_generator.build_request_str("rpc.discover")
+        response = rpc_server.process_request(request)
+        return rpc_generator.get_result_from_response(response)  # type: ignore
+
+    def test_info(self, discovered: dict):
+        info: dict = discovered["info"]
+        assert info.get("title") == "RPC Server"
+        assert info.get("version") == "0.1.0"
+
+    @pytest.fixture
+    def methods(self, discovered: dict) -> list:
+        return discovered.get("methods")  # type: ignore
+
+    def test_side_effect_method(self, methods: list):
+        m1: dict = methods[0]
+        m2 = methods[1]
+        assert m1["name"] == "sem"
+        assert m1.get("description") is None
+        assert m2["name"] == "side_effect_method"
+
+    def test_fail(self, methods: list):
+        m = methods[2]
+        assert m["name"] == "fail"
+        assert m["description"] == fail.__doc__
+
+    def test_rpc_discover_not_listed(self, methods: list):
+        for m in methods:
+            if m.get("name") == "rpc.discover":
+                raise AssertionError("rpc.discover is listed as a method!")
diff --git a/tests/management/test_starter.py b/tests/management/test_starter.py
index 01425ff7..6b5931d5 100644
--- a/tests/management/test_starter.py
+++ b/tests/management/test_starter.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 from unittest.mock import MagicMock
 
 import pytest
diff --git a/tests/utils/test_base_communicator.py b/tests/utils/test_base_communicator.py
index ef040282..50779cc8 100644
--- a/tests/utils/test_base_communicator.py
+++ b/tests/utils/test_base_communicator.py
@@ -22,6 +22,7 @@
 # THE SOFTWARE.
 #
 
+from __future__ import annotations
 import logging
 import time
 from typing import Optional
@@ -31,7 +32,7 @@
 
 from pyleco.test import FakeSocket
 from pyleco.core.message import Message, MessageTypes
-from pyleco.core.rpc_generator import RPCGenerator
+from pyleco.json_utils.rpc_generator import RPCGenerator
 from pyleco.errors import DUPLICATE_NAME, ErrorResponse
 
 from pyleco.utils.base_communicator import BaseCommunicator
diff --git a/tests/utils/test_communicator.py b/tests/utils/test_communicator.py
index 1d9c84de..6643abde 100644
--- a/tests/utils/test_communicator.py
+++ b/tests/utils/test_communicator.py
@@ -24,13 +24,13 @@
 
 from unittest.mock import MagicMock
 
-from jsonrpcobjects.errors import JSONRPCError
 import pytest
 
 from pyleco.core import VERSION_B
 from pyleco.core.message import Message, MessageTypes
 from pyleco.errors import NOT_SIGNED_IN, NODE_UNKNOWN
 from pyleco.core.serialization import serialize_data
+from pyleco.json_utils.errors import JSONRPCError
 
 from pyleco.utils.communicator import Communicator
 from pyleco.test import FakeSocket, FakeContext
diff --git a/tests/utils/test_coordinator_utils.py b/tests/utils/test_coordinator_utils.py
index b72891a4..1c6d8c15 100644
--- a/tests/utils/test_coordinator_utils.py
+++ b/tests/utils/test_coordinator_utils.py
@@ -22,13 +22,13 @@
 # THE SOFTWARE.
 #
 
-from jsonrpcobjects.objects import Request, ResultResponse, ErrorResponse
 import pytest
 
 from pyleco.test import FakeContext
 from pyleco.core.message import Message, MessageTypes
 from pyleco.errors import CommunicationError, NOT_SIGNED_IN, DUPLICATE_NAME
 from pyleco.utils.coordinator_utils import ZmqNode, ZmqMultiSocket, Node, Directory, FakeNode
+from pyleco.json_utils.json_objects import Request, ResultResponse, ErrorResponse
 
 
 class TestZmqMultiSocket:
diff --git a/tests/utils/test_message_handler.py b/tests/utils/test_message_handler.py
index 1fa4e24b..a84e38c3 100644
--- a/tests/utils/test_message_handler.py
+++ b/tests/utils/test_message_handler.py
@@ -27,7 +27,6 @@
 import time
 from typing import Optional
 
-from jsonrpcobjects.objects import Request, ResultResponse
 import pytest
 
 from pyleco.core import VERSION_B
@@ -36,8 +35,8 @@
 from pyleco.core.internal_protocols import CommunicatorProtocol
 from pyleco.core.serialization import serialize_data
 from pyleco.test import FakeContext, FakePoller
-from pyleco.errors import (NOT_SIGNED_IN, DUPLICATE_NAME, NODE_UNKNOWN, RECEIVER_UNKNOWN,
-                           ErrorResponse, DataError)
+from pyleco.errors import NOT_SIGNED_IN, DUPLICATE_NAME, NODE_UNKNOWN, RECEIVER_UNKNOWN
+from pyleco.json_utils.json_objects import Request, ResultResponse, ErrorResponse, DataError
 
 from pyleco.utils.message_handler import MessageHandler, SimpleEvent