alex-kudrya / laravel-jsonrpc
JSON RPC 2.0 for Laravel
Requires
- php: >=8.2
- laravel/framework: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- larastan/larastan: ^2.9|^3.0
- laravel/pint: ^1.24
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.5|^12.5|^13.0
README

JSON-RPC 2.0 for Laravel
A Laravel package that exposes one HTTP endpoint for JSON-RPC 2.0 calls, batches, and optional notifications. It dispatches built-in methods, explicitly registered RPC methods, or Laravel controllers.
See the JSON-RPC 2.0 specification for the protocol itself.
Compatibility
This package requires PHP 8.2 or newer and supports Laravel 10, 11, 12, and 13.
Upgrading from v1 is designed to require no code changes for ordinary requests that already contain id and params. See UPGRADE.md for the behavioral differences.
Installation
composer require alex-kudrya/laravel-jsonrpc:^2.0
Laravel package discovery registers the service provider automatically. If discovery is disabled, register it manually:
// config/app.php
'providers' => [
AlexKudrya\LaravelJsonRpc\Providers\JsonRpcServiceProvider::class,
],
Publishing is optional because the package merges its defaults at runtime:
php artisan vendor:publish \
--provider=AlexKudrya\\LaravelJsonRpc\\Providers\\JsonRpcServiceProvider
Publish only selected resources:
php artisan vendor:publish \
--provider=AlexKudrya\\LaravelJsonRpc\\Providers\\JsonRpcServiceProvider \
--tag=json-rpc-config
php artisan vendor:publish \
--provider=AlexKudrya\\LaravelJsonRpc\\Providers\\JsonRpcServiceProvider \
--tag=json-rpc-ai-guidelines
Endpoint and request envelope
The package registers one POST endpoint. Its default path is:
/json-rpc/v1
The path is controlled by json_rpc.api_prefix. RPC methods always belong in the JSON body, not in the URL.
{
"jsonrpc": "2.0",
"method": "Product@create",
"params": {
"name": "pencil",
"price": 1.5
},
"id": "product-create-1"
}
Successful response:
{
"jsonrpc": "2.0",
"result": {
"id": 42,
"name": "pencil"
},
"id": "product-create-1"
}
Request members:
| Member | Required | Description |
|---|---|---|
jsonrpc | yes | Must be the string "2.0". |
method | yes | Built-in name, registry alias, or a convention name such as Product@create. |
params | no | Object or array passed to the controller. Missing params becomes an empty array. |
id | configurable | String, integer, or null. The default policy requires the member. |
The response echoes the exact string, integer, or null ID. Batch consumers must correlate responses with requests by ID because notification responses are omitted.
Configuration
The main defaults are:
return [
'api_prefix' => env('JSON_RPC_PREFIX', 'json-rpc/v1'),
'route_middleware' => ['api'],
'controllers_root_namespace' => 'App\\Http\\Controllers\\JsonRpc\\',
'controllers_postfix' => 'Controller',
'controllers_method_delimiter' => '@',
'methods' => [],
'allow_convention_dispatch' => true,
'id_required' => env('JSON_RPC_ID_REQUIRED', true),
'max_batch_size' => env('JSON_RPC_MAX_BATCH_SIZE', 100),
'additional_high_level_parameters' => [],
'auth_required' => env('JSON_RPC_AUTH', false),
'auth_handler' => [
AlexKudrya\LaravelJsonRpc\JsonRpcAuthPlaceholder::class,
'handle',
],
'no_auth_methods' => [],
'error_logging' => false,
'expose_exception_trace' => env(
'JSON_RPC_EXPOSE_EXCEPTION_TRACE',
env('APP_DEBUG', false),
),
];
An older published config file does not have to be republished: missing top-level keys are supplied by the package defaults.
Method resolution
Methods are resolved in this order:
- Package built-ins such as
ping. - Exact aliases in
json_rpc.methods. Controller@methodconvention fallback whenallow_convention_dispatchistrue.
Explicit registry
An explicit registry is additive and takes precedence over convention dispatch:
// config/json_rpc.php
use App\Http\Controllers\JsonRpc\ProductController;
'methods' => [
'catalog.product.create' => [ProductController::class, 'create'],
'Product@create' => [ProductController::class, 'create'],
],
You can require registry-only dispatch:
'allow_convention_dispatch' => false,
Invalid registered targets are treated as server configuration errors. The package does not fall back to convention dispatch for a malformed entry with the same name.
Convention dispatch
With the default configuration:
Product@create
resolves to:
App\Http\Controllers\JsonRpc\ProductController::create()
Subnamespaces remain supported, for example Admin\Product@get. Only existing public non-magic methods are dispatchable. Private, protected, and __* methods are rejected.
Controllers and validation
A controller may receive raw params:
public function create(array $params): array
{
return ['name' => $params['name']];
}
It may have no arguments:
public function status(): array
{
return ['status' => 'ok'];
}
For Laravel validation and authorization, extend the package request:
namespace App\Http\Requests\JsonRpc;
use AlexKudrya\LaravelJsonRpc\Requests\JsonRpcRequest;
class CreateProductRequest extends JsonRpcRequest
{
protected function prepareForValidation(): void
{
$this->merge([
'name' => trim((string) $this->input('name')),
]);
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'price' => ['required', 'numeric', 'min:0'],
];
}
}
The controller can also receive additional container dependencies:
use App\Http\Requests\JsonRpc\CreateProductRequest;
use App\Services\ProductService;
public function create(
CreateProductRequest $request,
ProductService $products,
): array {
$product = $products->create($request->validated());
return $product->toArray();
}
Input access has two intentional views:
$request->params()and$request->param('name')return the original raw JSON-RPC params.$request->input(),$request->all(), and$request->validated()use the normal Laravel FormRequest lifecycle, includingprepareForValidation().
Use $request->validated() for persistence. Protocol metadata remains available through $request->method(), $request->method(true), $request->jsonrpc(), and $request->id().
Laravel authorize(), after(), passedValidation(), user/route resolvers, middleware attributes, headers, and the redirector/container lifecycle are preserved.
Notifications and ID policy
The backward-compatible default requires the id member:
'id_required' => true,
A missing ID then returns Invalid Request.
To enable JSON-RPC notifications:
'id_required' => false,
A notification omits id entirely:
{
"jsonrpc": "2.0",
"method": "Audit@record",
"params": {
"event": "signed-in"
}
}
The method is executed, but the endpoint returns HTTP 204 with no body. An explicit "id": null is not treated as a notification and receives a normal response.
Batch requests
A batch is a non-empty JSON array. Calls are processed sequentially; one failed call does not prevent later calls.
[
{
"jsonrpc": "2.0",
"method": "Product@get",
"params": {"id": 1},
"id": "get-1"
},
{
"jsonrpc": "2.0",
"method": "Audit@record",
"params": {"event": "product-read"}
},
{
"jsonrpc": "2.0",
"method": "ping",
"id": 300
}
]
When notifications are enabled, the notification is executed but omitted from the response:
[
{
"jsonrpc": "2.0",
"result": {"id": 1, "name": "pencil"},
"id": "get-1"
},
{
"jsonrpc": "2.0",
"result": {"message": "pong"},
"id": 300
}
]
Batch rules:
- An empty batch returns one
Invalid Requestobject. - Invalid scalar elements produce their own
Invalid Requestresponses withid: null. - A notification-only batch returns HTTP 204.
- The default
max_batch_sizeis100; an oversized batch is rejected before dispatch. - Set
max_batch_sizetonullto disable the size limit.
Errors
Protocol and server errors use a consistent object:
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"name": [
"The name field is required."
]
}
},
"id": "product-create-1"
}
| Condition | Code | Message |
|---|---|---|
| Malformed JSON | -32700 | Parse error |
| Invalid request envelope | -32600 | Invalid Request |
| Unknown method | -32601 | Method not found |
| Invalid params or Laravel validation | -32602 | Invalid params |
| Unhandled controller/configuration failure | -32603 | Internal error |
| Batch limit exceeded | -32000 | Batch size limit exceeded |
| Package auth denied | -32001 | Unauthorized |
| FormRequest authorization denied | -32003 | Forbidden |
JSON-RPC errors use HTTP 200. Middleware may still return its own HTTP status before the endpoint is reached.
Domain errors can keep application-specific codes:
use AlexKudrya\LaravelJsonRpc\Exceptions\JsonRpcException;
throw new JsonRpcException(
message: 'Product is archived',
code: 1201,
additional_data: ['product_id' => 42],
);
Authentication and middleware
For new applications, prefer normal Laravel middleware:
'route_middleware' => [
'api',
'auth:sanctum',
'throttle:api',
],
'auth_required' => false,
The middleware stack applies to the single endpoint. It can also attach tenancy, logging, tracing, or other request context.
The package auth handler remains available for backward compatibility and method-level bypasses:
'auth_required' => true,
'auth_handler' => [App\Auth\JsonRpcAuth::class, 'handle'],
'no_auth_methods' => ['ping'],
The configured public handler may accept zero, one, or two arguments:
public function handle(): bool
public function handle(array $input): bool
public function handle(array $input, array $headers): bool
Returning false produces Unauthorized. Handler configuration/runtime failures produce Internal error.
Error logging and diagnostics
Enable package error logging with:
'error_logging' => true,
The package uses the application-defined logging.channels.json_rpc unchanged. If the application has not defined it, the provider installs a daily-file default.
Generic exception messages and traces are hidden by default. Expose traces only in trusted development environments:
'expose_exception_trace' => true,
A logging failure never changes the protocol response.
Swagger / OpenAPI
Document JSON-RPC as one POST endpoint. JsonRpcOpenApi produces method-specific request/result schemas while retaining single and batch shapes:
use AlexKudrya\LaravelJsonRpc\OpenApi\JsonRpcOpenApi;
$openapi = JsonRpcOpenApi::document([
JsonRpcOpenApi::method(
method: 'Product@create',
paramsSchema: [
'type' => 'object',
'required' => ['name'],
'properties' => [
'name' => ['type' => 'string'],
'price' => ['type' => 'number'],
],
],
resultSchema: [
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
'name' => ['type' => 'string'],
],
],
options: [
'summary' => 'Create product',
'params_example' => ['name' => 'pencil', 'price' => 1.5],
],
),
], [
'title' => 'Product JSON-RPC API',
'version' => '2.0.0',
'id_required' => config('json_rpc.id_required', true),
]);
The generated document includes:
- optional
params; - required or optional
idaccording toid_required; - string, integer, and
nullIDs; - standard error objects with
code,message, and optionaldata; - single request/response and batch shapes;
- HTTP 204 for notifications.
Duplicate normalized schema/example names and invalid method definitions raise InvalidArgumentException instead of silently overwriting documentation.
Testing
Use the package trait in consumer feature tests:
use AlexKudrya\LaravelJsonRpc\Testing\JsonRpcRequestTrait;
class ProductJsonRpcTest extends TestCase
{
use JsonRpcRequestTrait;
public function test_create(): void
{
$response = $this->jsonRpcRequest(
controller: ProductController::class,
method: 'create',
params: ['name' => 'pencil'],
id: 1001,
);
$response->assertOk();
$response->assertJsonPath('result.name', 'pencil');
$response->assertJsonPath('id', 1001);
}
}
Notification helper:
config()->set('json_rpc.id_required', false);
$response = $this->jsonRpcNotification(
controller: AuditController::class,
method: 'record',
params: ['event' => 'signed-in'],
);
$response->assertNoContent();
The helper preserves controller subnamespaces and sends requests to the configured endpoint.
Local quality checks
composer validate --strict --no-check-publish
composer check
composer audit