6. Groups
What You'll Learn
- Organize methods with prefixes
- Create hierarchical method namespaces
- Implement middleware with custom MethodGroups
Basic Group Registration
from dataclasses import dataclass
from jsonrpc import JSONRPC, MethodGroup, Method
# Math methods
@dataclass
class BinaryOpParams:
a: float
b: float
class Add(Method):
def execute(self, params: BinaryOpParams) -> float:
return params.a + params.b
class Multiply(Method):
def execute(self, params: BinaryOpParams) -> float:
return params.a * params.b
# String methods
@dataclass
class StringParams:
text: str
class Uppercase(Method):
def execute(self, params: StringParams) -> str:
return params.text.upper()
class Reverse(Method):
def execute(self, params: StringParams) -> str:
return params.text[::-1]
# Setup
rpc = JSONRPC(version='2.0')
math_group = MethodGroup()
math_group.register('add', Add())
math_group.register('multiply', Multiply())
rpc.register('math', math_group)
string_group = MethodGroup()
string_group.register('upper', Uppercase())
string_group.register('reverse', Reverse())
rpc.register('string', string_group)
Request:
Response:
{
"jsonrpc": "2.0",
"method": "string.reverse",
"params": {"text": "hello"},
"id": 2
}
Response:
Hierarchical Namespaces
from dataclasses import dataclass
from jsonrpc import JSONRPC, MethodGroup, Method
@dataclass
class UserIdParams:
user_id: int
@dataclass
class DeletedResult:
deleted: int
@dataclass
class UserSummary:
id: int
@dataclass
class ProfileResult:
user_id: int
name: str
# Admin group
class DeleteUser(Method):
def execute(self, params: UserIdParams) -> DeletedResult:
return DeletedResult(deleted=params.user_id)
class ListUsers(Method):
def execute(self, params: None) -> list[UserSummary]:
return [UserSummary(id=1), UserSummary(id=2)]
# Public group
class GetProfile(Method):
def execute(self, params: UserIdParams) -> ProfileResult:
return ProfileResult(user_id=params.user_id, name="John")
# Setup hierarchical structure
rpc = JSONRPC(version='2.0')
# admin.user.*
admin_user_group = MethodGroup()
admin_user_group.register('delete', DeleteUser())
admin_user_group.register('list', ListUsers())
admin_group = MethodGroup()
admin_group.register('user', admin_user_group)
# public.user.*
public_user_group = MethodGroup()
public_user_group.register('profile', GetProfile())
public_group = MethodGroup()
public_group.register('user', public_user_group)
rpc.register('admin', admin_group)
rpc.register('public', public_group)
Methods available:
- admin.user.delete
- admin.user.list
- public.user.profile
Request:
{
"jsonrpc": "2.0",
"method": "admin.user.delete",
"params": {"user_id": 42},
"id": 1
}
Middleware
MethodGroup is the extension point for cross-cutting concerns. Override around_call() to inject behavior before and after every call into the group's subtree — logging, caching, rate limiting, authentication guards, and more.
class Unauthenticated(JSONRPCError):
code = -32010
message = 'Authentication required'
class RequireAuthGroup(MethodGroup):
def around_call(self, call, context, call_next):
if context.user_id is None:
raise Unauthenticated()
return call_next(context)
Refuse with a JSONRPCError subclass. Any other exception — PermissionError,
a framework's own Forbidden — is answered with a bare -32603 Internal error
and logged with a traceback, so the caller cannot tell a refusal from a fault and
neither can you.
Groups compose cleanly: wrap one group inside another to layer behaviors without touching method logic. Every group on the path runs, outermost first, so a guard mounted at admin covers admin.user.delete and everything else below it.
around_call(), not execute_method()
execute_method() runs only on the group a method is registered on. It is the right hook for changing how that group invokes its own methods, and the wrong one for guarding a namespace — mounting a group that overrides it but owns only subgroups is rejected at registration for exactly that reason.
→ Middleware - Reference implementations: logging, caching, rate limiting, auth guards
Key Points
- MethodGroup: Organizes methods with prefix
- Hierarchical: Groups can contain groups (unlimited depth)
- Middleware: Override
around_call()for behavior covering the whole subtree - Composition: Combine multiple groups for layered functionality
- Separation: Business logic (Method) vs routing (MethodGroup)
- Singletons: one instance is registered in exactly one place and shared across every request and thread — keep per-request state in
context, never onself
Real-World Usage
Use groups for:
- API versioning: v1.*, v2.*
- Access control: public.*, admin.*
- Feature separation: users.*, orders.*, payments.*
What's Next?
→ OpenAPI — Auto-generate API documentation