3. Parameters
What You'll Learn
- Define required and optional parameters
- Use default values
- Handle validation errors
Required Parameters
from dataclasses import dataclass
from jsonrpc import JSONRPC, Method
@dataclass
class CreateUserParams:
email: str
username: str
age: int
@dataclass
class CreateUserResult:
user_id: int
email: str
username: str
class CreateUser(Method):
def execute(self, params: CreateUserParams) -> CreateUserResult:
return CreateUserResult(
user_id=123,
email=params.email,
username=params.username,
)
rpc = JSONRPC(version='2.0')
rpc.register('create_user', CreateUser())
Request:
{
"jsonrpc": "2.0",
"method": "create_user",
"params": {
"email": "user@example.com",
"username": "john_doe",
"age": 25
},
"id": 1
}
Response:
{
"jsonrpc": "2.0",
"result": {
"user_id": 123,
"email": "user@example.com",
"username": "john_doe"
},
"id": 1
}
Optional Parameters with Defaults
from dataclasses import dataclass
from jsonrpc import JSONRPC, Method
@dataclass
class SearchParams:
query: str
limit: int = 10
offset: int = 0
sort: str = "relevance"
@dataclass
class SearchResult:
query: str
limit: int
offset: int
sort: str
total: int
class Search(Method):
def execute(self, params: SearchParams) -> SearchResult:
return SearchResult(
query=params.query,
limit=params.limit,
offset=params.offset,
sort=params.sort,
total=0,
)
rpc = JSONRPC(version='2.0')
rpc.register('search', Search())
Request (minimal — only required field):
{
"jsonrpc": "2.0",
"method": "search",
"params": {"query": "python"},
"id": 1
}
Response (defaults applied):
{
"jsonrpc": "2.0",
"result": {
"query": "python",
"limit": 10,
"offset": 0,
"sort": "relevance",
"total": 0
},
"id": 1
}
Request (custom values):
{
"jsonrpc": "2.0",
"method": "search",
"params": {
"query": "python",
"limit": 50,
"sort": "date"
},
"id": 2
}
No Parameters
from dataclasses import dataclass
from datetime import datetime
from jsonrpc import JSONRPC, Method
@dataclass
class TimeResult:
timestamp: str
class GetServerTime(Method):
def execute(self, params: None) -> TimeResult:
return TimeResult(timestamp=datetime.now().isoformat())
rpc = JSONRPC(version='2.0')
rpc.register('server_time', GetServerTime())
Request:
Validation Errors
Invalid type:
{
"jsonrpc": "2.0",
"method": "create_user",
"params": {
"email": "user@example.com",
"username": "john_doe",
"age": "twenty-five"
},
"id": 1
}
Error response:
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Parameter 'age' expected type 'int', got 'str'"
},
"id": 1
}
Missing required field:
{
"jsonrpc": "2.0",
"method": "create_user",
"params": {
"email": "user@example.com"
},
"id": 2
}
Error response:
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Missing required parameter: 'username'"
},
"id": 2
}
Validating values, not just types
The type check answers "is this an int". Whether it is an int you will accept is
your rule, and the place for it is __post_init__ — ordinary Python, no library
API involved:
from dataclasses import dataclass
from jsonrpc.errors import InvalidParamsError
@dataclass
class CreateUserParams:
"""A new user account."""
name: str
age: int
def __post_init__(self):
if not self.name.strip():
raise InvalidParamsError('name must not be empty')
if not 13 <= self.age <= 120:
raise InvalidParamsError('age must be between 13 and 120')
{"jsonrpc": "2.0", "method": "users.create", "params": {"name": "Ada", "age": -1}, "id": 1}
→ {"jsonrpc": "2.0", "error": {"code": -32602, "message": "age must be between 13 and 120"}, "id": 1}
Worth knowing about that message: it reaches the caller verbatim. That is the point — the reason to write it is that someone reads it — but it means the text is public. Do not put internal detail in it.
Three notes on the mechanics:
- Raise
InvalidParamsError, notValueError, when you want the caller to read your message. Both become-32602; only the first carries its text outward. A plainValueErrorgets the fixed messageInvalid params: rejected by the parameter typeand its own text goes to the log — because theValueErroryou did not write is the far more common one:datetime.fromisoformat,int,Decimal,UUIDandjson.loadsall put the input they were handed into the message they raise, and that input came from the caller.InvalidParamsErroralso takesdata=for a machine-readable payload. - Do not use
assert: it disappears underpython -O, and the response would be-32603, which is deliberate — a check that vanishes in production should not look like a working one. __post_init__validates; it does not change the declared type. The annotation is what mypy reads and what OpenAPI publishes, so keep the field's type honest. On afrozen=Truedataclass, assigning at all needsobject.__setattr__.
Nested dataclasses work the same way — an InvalidParamsError raised inside one
reports as -32602 for the whole request.
Types JSON cannot express
Parameters are limited to what JSON has: strings, numbers, booleans, arrays,
objects, null — plus dataclasses built from those. datetime, date, Enum,
Decimal, UUID, tuple, set and bytes are not accepted as parameter
annotations: the value arrives as a str, and a str is never an instance of
datetime, so no value the caller could send would ever be accepted. Declaring
one raises TypeError where the method class is defined, rather than answering
-32602 to every call for the life of the process.
Take the wire type and convert where you validate:
from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal, InvalidOperation
from jsonrpc.errors import InvalidParamsError
@dataclass
class ReportParams:
"""A report over a date range."""
since: str # ISO-8601, e.g. "2024-01-01T00:00:00"
budget: str # decimal string, e.g. "10.50"
parsed_since: datetime = field(init=False)
parsed_budget: Decimal = field(init=False)
def __post_init__(self):
try:
self.parsed_since = datetime.fromisoformat(self.since)
except ValueError:
raise InvalidParamsError('since must be ISO-8601') from None
try:
self.parsed_budget = Decimal(self.budget)
except InvalidOperation:
raise InvalidParamsError('budget must be a decimal string') from None
init=False fields are not parameters — the caller cannot set them and the spec
does not list them — so they are the right place to keep the converted value.
Malformed input produces -32602 with your message, not a -32603.
Note what those two except clauses are for. InvalidOperation and the
ValueError from fromisoformat both already become -32602 on their own — but
with a fixed message, because their own text embeds the string the caller sent
(Invalid isoformat string: '<whatever they typed>'). Catching them and raising
your own sentence is what puts something useful on the wire.
The annotation stays str, which is honest: str is what the caller sends and
what the published schema describes.
Going the other way — returning a datetime or a Decimal — is a
serialization problem, and there the library does have a hook. See
serialize().
Complex Example
from dataclasses import dataclass
from jsonrpc import JSONRPC, Method
@dataclass
class FilterParams:
keyword: str
min_price: float | None = None
max_price: float | None = None
categories: list[str] | None = None
in_stock: bool = True
sort_by: str = "price"
page: int = 1
per_page: int = 20
@dataclass
class FilterResult:
keyword: str
page: int
per_page: int
total: int
class FilterProducts(Method):
def execute(self, params: FilterParams) -> FilterResult:
return FilterResult(
keyword=params.keyword,
page=params.page,
per_page=params.per_page,
total=0,
)
rpc = JSONRPC(version='2.0')
rpc.register('filter_products', FilterProducts())
Request:
{
"jsonrpc": "2.0",
"method": "filter_products",
"params": {
"keyword": "laptop",
"min_price": 500.0,
"max_price": 1500.0,
"categories": ["electronics", "computers"],
"page": 2,
"per_page": 50
},
"id": 1
}
Key Points
- Required fields: No default value
- Optional fields: Provide a default value
- No params: Use
params: Nonetype hint - Type validation: Automatic based on type hints
- Error code:
-32602for Invalid params
IDE Support
Dataclasses give you autocomplete in IDEs like VS Code and PyCharm!
What's Next?
→ Nested Types - Complex data structures