is a json-based protocol for rpc. See JSON-RPC-Spec.
As developer is sometime difficult to create a good RESTful application for the current problem.
Think about it:
- check if a user has a certain permission
- change state of a proerty
- run a long-running job, e.g. scan a file for viruses
- do something within a transaction
Yes, all the above problems can be solved in a RESTful way, but not all developers are Stefan Tikov and able to think in resources to solve the problems. For those who cannot think in resources, json-rpc can be a solution as it defines a structure that allows procedural thinking. Without the need to invent everything from scratch.
If you have clean resources and need a way to access them via http. Think of KODI:
- Retrieve all available movie:
make a simple GET request to the movie endpoint instead of calling a list method via rpc. - search for a movie:
make a GET request with query parameters to the movie endpoint instead of calling a list method with search parameters via rpc
All requests are POST
, a welcome from SOAP
, but GraphQL
does the same. The provider uses 200 (OK) for all responses, so it is not possible to distinguish between success and failure.
Here are some examples of bad RESTful solutions for the above example and a json-rpc solution for it.
Requirement: we need a way to check if a caller has permission to do something.
- as a consumer, we perform a GET call against a Uri to verify that we have permission to
e.g. GET /permissions/listUsers - the provider now checks if the caller has the requested permission and returns
- different HTTP status codes
- 200 (OK) - if the caller has the permission
- 403 (FORBIDDEN) - if the caller does not have the permission
- an HTTP response with status code 200 (OK) and a Boolean in the body
- different HTTP status codes
- the consumer must now implement a function to check the response and remember if the current user is able to perform a particular action
As a provider, we can use HATEOAS to tell the consumer which endpoints are available to the current caller. Each response can tell the consumer what actions are possible now.
- as a consumer, we ask the provider what the endpoints are (we see the provider as the master resource), so we do a GET againts the entry-point
- as a provider, we now check for which endpoints the caller has permission and return it as an object to the consumer, e.g.
{ "listUsers":"/api/listsUsers"}
. - the consumer can now use this object to derive the possible next actions
This type of API is the most decoupled type. The provider can change the Uri under which a particular action can be performed, and the consumer can decide which action it needs and ignore all others. But we have to work very hard to get it right. It would go beyond the scope to go into more detail.
- as a consumer we run a POST with
{"jsonrpc": "2.0", "method": "checkPermission", "params": ["listUsers"], "id": 1}
as body - the provider now checks if the caller has the requested permission and returns
{"jsonrpc": "2.0", "result": true, "id": 1}
{"jsonrpc": "2.0", "result": false, "id": 1}
- the consumer must now implement a function to check the response and remember if the current user is able to perform a particular action
In contrast to the naive way: We just define better communication, what kind of communication we use and don't provide an API that looks like RESTful but isn't 100%
Requirement: We need a quick way to change the state in one direction, like change the verification-state of a user to verified.
This example is kind of a poor understanding of RESTful, but I've seen it many times in real world api's. And yes we should never use json-rpc to solve it, but we do it here.
- as a provider, we define an endpoint to change a property of a resource in one direction, e.g.
/persons/{id}/markAsVerified
. As a method we can use PUT or POST, it just doesn't matter- define 204 (NOCONTENT) for the response
- define 200 (OK) with the changed resource as body
- as a consumer, we call the above endpoint to change the resource
- provider returns 204
- manipulate our state in our resource.
- reload the resource in question from the provider
- provider returns 200
- replace our resource with the new one
- update our resource with the values of the provider
- provider returns 204
- load as consumer the resource in question e.g. via
GET /persons/{id}
- as provider return the full resource
{"id": 5, verified: "false"}
- as a consumer, modify the property in question and return it as a whole to the provider via
PUT
e.g.PUT {"id": 5, verified: "true"} /people/5
- as a provider, apply the manipulation to the resource and return 204 (NOCONTENT) if successful
- as a consumer, check if we have not received 204, and then decide whether to resend or cancel the change
Yes, the whole resource is transferred twice, but we can make sure that the two ends have the same view on the resource. The consumer had sent his to the provider and the provider has confirms or denies the update and also communicates this to the consumer so that the consumer can question his view.
- as a consumer we run a
POST
with{"jsonrpc": "2.0", "method": "markAsVerified", "params": ["5"], "id": 1}
as body - as provider, we do the manipulation
- do it as notification and return nothing
- return the result of the manipulation
{"jsonrpc": "2.0", "result": {"id": 5, verified: "true"}, "id": 1}
- as consumer we must now sync our state
- use a notification
- manipulate our state in our resource.
- reload the resource in question from the provider
- use the result from the provider
- replace our resource with the new one
- update our resource with the values of the provider
- use a notification
In contrast to the naive way: We just define better communication, what kind of communication we use and don't provide an API that looks like RESTful but isn't 100%.
Requirement: We had to scan files for virus.
- as a consumer POST the contents of the file to the scanner aka provider
- as a provider, scan the provided content with one or more scanners and return the result (scannresult as status code or body possible)
It is a simple, synchronous method where the provider leaves it up to the consumer to decide if they need a asynchronous method.
- as a consumer POST the contents of the file to the scanner aka provider
- s a provider, create an order or resource for the request and return the id and location of the resource with 201 (CREATED)
- as a consumer, query the status of the created order, whether it is finished (HEAD or GET possible)
- as provider if the job is finised return the location of the result to the consumer
- as consumer retrieve the result
As a provider, we must now provide a memory of all our orders and this longer than necessary. Because it is not certain that the consumer will need the result of the order again later. As consumers, we have to poll for the result, which can lead to a higher load on the provider side.
It is possible to create the method as a notification and as a synchronous method. When using Notifaction we need polling on the consumer side.
A synchron sample
- as a consumer we run a
POST
with{"jsonrpc": "2.0", "method": "scan", "params": ["filecontentAsBase64"], "id": 1}
as body - as a provider, scan the provided content with one or more scanners and return
{"jsonrpc": "2.0", "result": {virusfree: "true"}, "id": 1}
And again, we have clean communication about how our API works. The consumer doesn't have to learn anything new. And the provider doesn't have to worry about how to communicate the result to the consumer either. In my eyes, this is one of the best use cases for using rpc style.
Requirement: We had to track changes together.
- as a consumer, perform a
POST {}
to create a transaction - as provider return the location of the new created transaction
- as a consumer, perform all necessary steps with the newly created transaction
It depends on who determines when a transaction is finished. Both force the provider to store the data longer than necessary. He cannot be sure that no consumer needs the status of the transaction anymore.
- as a consumer we run a
POST
with{"jsonrpc": "2.0", "method": "insideTransaction", "params": ["action1", "action2"]
as body - as the provider, create a new transaction and do all the things you want in this
This example uses a notification that allows the provider to do anything without having to send anything back to the consumer. And again, we have clean communication about how our API works. The consumer doesn't have to learn anything new. In my eyes, this is a good use cases for using rpc style.
see samples.
Add maven dependency
<dependency>
<groupId>io.github.sebastian-toepfer.json.rpc</groupId>
<artifactId>json-rpc-boundary</artifactId>
<version>0.6.0</version>
</dependency>
provide a rpc-runtime via CDI:
@Produces
@RequestScoped
public JsonRpcRuntime jsonRpcRuntime() {
return new DefaultJsonRpcRuntime(
new DefaultJsonRpcExecutionContext()
.withMethod(
new DefaultJsonRpcMethod(
"subtract",
List.of("minuend", "subtrahend"),
params -> Json.createValue(params.getInt("minuend") - params.getInt("subtrahend"))
)
)
);
}
register the rpc endpoint as jax-rs resource
@ApplicationPath("")
public class SampleApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
return Set.of(RpcResource.class);
}
}
Add maven dependency
<dependency>
<groupId>io.github.sebastian-toepfer.json.rpc</groupId>
<artifactId>json-rpc-boundary</artifactId>
<version>0.6.0</version>
</dependency>
enable jaxrs extension
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jsonb</artifactId>
</dependency>
extends application.properties (to index the boundary)
quarkus.index-dependency.json-rpc-boundary.group-id=io.github.sebastian-toepfer.json.rpc
quarkus.index-dependency.json-rpc-boundary.artifact-id=json-rpc-boundary
provide a rpc-runtime via CDI:
@Produces
@RequestScoped
public JsonRpcRuntime jsonRpcRuntime() {
return new DefaultJsonRpcRuntime(
new DefaultJsonRpcExecutionContext()
.withMethod(
new DefaultJsonRpcMethod(
"subtract",
List.of("minuend", "subtrahend"),
params -> Json.createValue(params.getInt("minuend") - params.getInt("subtrahend"))
)
)
);
}
Add maven dependency
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>json-rpc-spring-integration-starter</artifactId>
<version>0.6.0</version>
</dependency>
register the rpc endpoint as jax-rs resource
@Component
public class JerseyConfig extends ResourceConfig {
public JerseyConfig() {
register(RpcResource.class);
}
}
provide a rpc-runtime via Spring-DI
@Bean
public JsonRpcRuntime jsonRpcRuntime() {
return new DefaultJsonRpcRuntime(
new DefaultJsonRpcExecutionContext()
.withMethod(
new DefaultJsonRpcMethod(
"subtract",
List.of("minuend", "subtrahend"),
params -> Json.createValue(params.getInt("minuend") - params.getInt("subtrahend"))
)
)
);
}
a dynamic way of describing the available methods. for more information see openrpc
Add maven dependency
<dependency>
<groupId>io.github.sebastian-toepfer.json.rpc.extension</groupId>
<artifactId>json-rpc-extension-openrpc</artifactId>
<version>0.7.0</version>
</dependency>
create the method context (used instead of DefaultJsonRpcExecutionContext)
new OpenRpcServiceDiscoveryJsonRpcExecutionContext(new InfoObject("test app", "1.0.0"))
.withMethod(
new DescribableJsonRpcMethod(
new MethodObject(
"list_pets",
List.of(
new ContentDescriptorOrReference.Object(
new ContentDescriptorObject(
"limit",
new JsonSchemaOrReference.Object(
JsonSchemas.load(Json.createObjectBuilder().add("type", "integer").build())
)
)
.withDescription("How many items to return at one time (max 100)")
.withRequired(false)
)
)
)
.withSummary("List all pets")
.withTags(List.of(new TagOrReference.Object(new TagObject("pets"))))
.withResult(
new MethodObjectResult.Object(
new ContentDescriptorObject(
"pets",
new JsonSchemaOrReference.Reference(
new ReferenceObject("#/components/schemas/Pets")
)
)
.withDescription("A paged array of pets")
)
),
params -> Json.createArrayBuilder().add("bunnies").add("cats").build()
)
)
all properties described at https://spec.open-rpc.org/#meta-json-schema can be used. The mandatory properties must be
specified as constructor parameters, optional parameters can be added via with methods such as
withDescription("")
. If a parameter can have different types, this is specified via an 'Or' WrapperObject.
e.g. a JsonSchema can be set via Reference or SchemaInstance:
new JsonSchemaOrReference.Reference(new RefereceObject(""))
or
new JsonSchemaOrReference.Object(new JsonSchemaObject())
.
As of version 0.7.0, it is no longer possible to describe method parameters via a reference or to use a reference as a parameter.
after deployment the avalible methods can be requested via:
{
"jsonrpc": "2.0",
"method": "rpc.discover",
"id": "1"
}
Use an existing spec:
JsonRpcExecutionContext<DescribableJsonRpcMethod> context = OpenRPCSpec
.load(OpenRPCSpecTest.class.getClassLoader().getResourceAsStream("petstore-openrpc.json"))
.map(in -> Json.createValue("list_pets"))
.toName("list_pets")
.map(in -> Json.createValue("create_pet"))
.toName("create_pet")
.asContext()
it is possible to add more method to this context.
Add maven dependency
<dependency>
<groupId>io.github.sebastian-toepfer.json.rpc.extension</groupId>
<artifactId>json-rpc-extension-micrometer</artifactId>
<version>0.7.0</version>
</dependency>
create the method context, which wrapped any other context
new ObservableJsonRpcExecutionContext<>(
registry, //e.g. new SimpleMeterRegistry()
new DefaultJsonRpcExecutionContext(), //or any other context
DefaultJsonRpcMethodMetrics.CALLCOUNT,
DefaultJsonRpcMethodMetrics.CALLTIME
)
or to prefix any metric
new ObservableJsonRpcExecutionContext<>(
registry, //e.g. new SimpleMeterRegistry()
new DefaultJsonRpcExecutionContext(), //or any other context
"prefix",
DefaultJsonRpcMethodMetrics.CALLCOUNT,
DefaultJsonRpcMethodMetrics.CALLTIME
)