type-safe json-rpc api
Armadillo
London, 2022
We are hiring :)

- fully decentralized remote working organisation
- software development based on research papers
- creators of cardano, first PoS blockchain built using evidence-based methods
- established collaboration with recognized universities such as Edinburgh and Stanford
scala
haskell
typescript
agda
How it started?
- a requirement to provide json-rpc api
- how do we document that api?
- how to keep that documentation up-to-date?
JSON-RPC protocol
- created in 2005 as a JSON based alternative to XML-RPC protocol
- last revision - 2.0 from 2013
- transport layer agnostic
- mainly used for inter-process communication
- generally quite simple, but there as some caveats
JSON-RPC protocol
--> data sent to Server
<-- data sent to ClientBasic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}https://www.jsonrpc.org/specification
JSON-RPC protocol
--> data sent to Server
<-- data sent to ClientBasic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}https://www.jsonrpc.org/specification
JSON-RPC protocol
--> data sent to Server
<-- data sent to ClientBasic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}https://www.jsonrpc.org/specification
JSON-RPC protocol
--> data sent to Server
<-- data sent to ClientBasic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}https://www.jsonrpc.org/specification
JSON-RPC protocol
--> data sent to Server
<-- data sent to ClientBasic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}https://www.jsonrpc.org/specification
JSON-RPC protocol
--> data sent to Server
<-- data sent to ClientBasic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}https://www.jsonrpc.org/specification
JSON-RPC protocol
--> data sent to Server
<-- data sent to ClientBasic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}https://www.jsonrpc.org/specification
JSON-RPC protocol
--> data sent to Server
<-- data sent to ClientBasic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}https://www.jsonrpc.org/specification
JSON-RPC protocol
--> data sent to Server
<-- data sent to ClientRequest with named parameters:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":{
"subtrahend":23,
"minuend":42
},
"id":3
}
<-- {"jsonrpc": "2.0", "result": 19, "id": 1}https://www.jsonrpc.org/specification
JSON-RPC protocol
--> data sent to Server
<-- data sent to ClientA notification:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":{
"subtrahend":23,
"minuend":42
}
}https://www.jsonrpc.org/specification
JSON-RPC protocol
--> data sent to Server
<-- data sent to ClientBatch request:
--> [
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
{"jsonrpc": "2.0", "method": "subtract", "params": [42,23], "id": "2"},
]
<-- [
{"jsonrpc": "2.0", "result": 7, "id": "1"},
{"jsonrpc": "2.0", "result": 19, "id": "2"},
]https://www.jsonrpc.org/specification
JSON-RPC protocol
--> data sent to Server
<-- data sent to ClientBatch request with a notification:
--> [
{"jsonrpc": "2.0", "method": "sum", "params": [1,2,4], "id": "1"},
{"jsonrpc": "2.0", "method": "notify_hello", "params": ["hello"]},
]
<-- [
{"jsonrpc": "2.0", "result": 7, "id": "1"},
]https://www.jsonrpc.org/specification
Our goals
- always up-to-date, automatically generated documentation
- fully support JSON-RPC 2.0 specification
- express endpoint as values
- ability to run it side-by-side with legacy code
- integration with multiple json libraries
- integration with multiple http servers
Why just not use tapir?
HTTP POST /
{
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
HTTP POST /
{
"jsonrpc":"2.0",
"method":"sum",
"params":[
42,
23
],
"id":1
}Our goals
- always up-to-date, automatically generated documentation
- fully support JSON-RPC 2.0 specification
- express endpoint as values
- ability to run it side-by-side with legacy code
- integration with multiple json libraries
- integration with multiple http servers
Why just not use tapir?
HTTP POST /
{
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
HTTP POST /
{
"jsonrpc":"2.0",
"method":"sum",
"params":[
42,
23
],
"id":1
}val jsonRpc = endpointOur goals
- always up-to-date, automatically generated documentation
- fully support JSON-RPC 2.0 specification
- express endpoint as values
- ability to run it side-by-side with legacy code
- integration with multiple json libraries
- integration with multiple http servers
Why just not use tapir?
HTTP POST /
{
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
HTTP POST /
{
"jsonrpc":"2.0",
"method":"sum",
"params":[
42,
23
],
"id":1
}val jsonRpc = endpoint.postOur goals
- always up-to-date, automatically generated documentation
- fully support JSON-RPC 2.0 specification
- express endpoint as values
- ability to run it side-by-side with legacy code
- integration with multiple json libraries
- integration with multiple http servers
Why just not use tapir?
HTTP POST /
{
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
HTTP POST /
{
"jsonrpc":"2.0",
"method":"sum",
"params":[
42,
23
],
"id":1
}val jsonRpc = endpoint.post.in(jsonBody[JsonRpcRequest])Our goals
- always up-to-date, automatically generated documentation
- fully support JSON-RPC 2.0 specification
- express endpoint as values
- ability to run it side-by-side with legacy code
- integration with multiple json libraries
- integration with multiple http servers
Why just not use tapir?
HTTP POST /
{
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
HTTP POST /
{
"jsonrpc":"2.0",
"method":"sum",
"params":[
42,
23
],
"id":1
}val jsonRpc = endpoint.post.in(jsonBody[JsonRpcRequest])
.out(jsonBody[Option[JsonRpcResponse]])Our goals
- always up-to-date, automatically generated documentation
- fully support JSON-RPC 2.0 specification
- express endpoint as values
- ability to run it side-by-side with legacy code
- integration with multiple json libraries
- integration with multiple http servers
Why just not use tapir?
HTTP POST /
{
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
HTTP POST /
{
"jsonrpc":"2.0",
"method":"sum",
"params":[
42,
23
],
"id":1
}Endpoint[JsonRpcRequest, JsonRpcError, Option[JsonRpcResponse]]Our goals
- always up-to-date, automatically generated documentation
- fully support JSON-RPC 2.0 specification
- express endpoint as values
- ability to run it side-by-side with legacy code
- integration with multiple json libraries
- integration with multiple http servers
Why just not use tapir?
https://github.com/softwaremill/tapir/issues/621
Meet armadillo
Declarative, type-safe json-rpc endpoints library
Meet armadillo
Basic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {
"jsonrpc":"2.0",
"result":19,
"id":1
} Meet armadillo
Basic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {
"jsonrpc":"2.0",
"result":19,
"id":1
}val subtract =
jsonRpcEndpoint(m"subtract")
Meet armadillo
Basic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {
"jsonrpc":"2.0",
"result":19,
"id":1
}val subtract =
jsonRpcEndpoint(m"subtract")
.in(
param[Int]("subtrahend")
)Meet armadillo
Basic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {
"jsonrpc":"2.0",
"result":19,
"id":1
}val subtract =
jsonRpcEndpoint(m"subtract")
.in(
param[Int]("subtrahend")
.and(param[Int]("minuend"))
)Meet armadillo
Basic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {
"jsonrpc":"2.0",
"result":19,
"id":1
}val subtract =
jsonRpcEndpoint(m"subtract")
.in(
param[Int]("subtrahend")
.and(param[Int]("minuend"))
)
.out[Int]("result of the subtraction")Meet armadillo
Basic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {
"jsonrpc":"2.0",
"result":19,
"id":1
}val subtract: JsonRpcEndpoint[(Int, Int), Unit, Int] =
jsonRpcEndpoint(m"subtract")
.in(
param[Int]("subtrahend")
.and(param[Int]("minuend"))
)
.out[Int]("result of the subtraction")Meet armadillo
Basic request:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
}
<-- {
"jsonrpc":"2.0",
"result":19,
"id":1
}val subtract: JsonRpcServerEndpoint[Id] =
jsonRpcEndpoint(m"subtract")
.in(
param[Int]("subtrahend")
.and(param[Int]("minuend"))
)
.out[Int]("result of the subtraction")
.serverLogic[Id] { case (subtrahend, minuend) =>
Right(subtrahend - minuend)
}Meet armadillo
Request with named parameters:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":{
"subtrahend":23,
"minuend":42
},
"id":3
}
<-- {
"jsonrpc":"2.0",
"result":19,
"id":1
}val subtract: JsonRpcServerEndpoint[Id] =
jsonRpcEndpoint(m"subtract")
.in(
param[Int]("subtrahend")
.and(param[Int]("minuend"))
)
.out[Int]("result of the subtraction")
.serverLogic[Id] { case (subtrahend, minuend) =>
Right(subtrahend - minuend)
}Meet armadillo
Request with named parameters:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":{
"subtrahend":23,
"minuend":42
},
"id":3
}
<-- {
"jsonrpc":"2.0",
"result":19,
"id":1
}val subtract: JsonRpcServerEndpoint[Id] =
jsonRpcEndpoint(m"subtract", ParamStructure.ByName)
.in(
param[Int]("subtrahend")
.and(param[Int]("minuend"))
)
.out[Int]("result of the subtraction")
.serverLogic[Id] { case (subtrahend, minuend) =>
Right(subtrahend - minuend)
}Meet armadillo
Notification:
--> {
"jsonrpc":"2.0",
"method":"subtract",
"params":{
"subtrahend":23,
"minuend":42
}
# no request id
}val subtract: JsonRpcServerEndpoint[Id] =
jsonRpcEndpoint(m"subtract")
.in(
param[Int]("subtrahend")
.and(param[Int]("minuend"))
)
.out[Int]("result of the subtraction")
.serverLogic[Id] { case (subtrahend, minuend) =>
Right(subtrahend - minuend)
}Meet armadillo
Batch request:
-->[
{
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
23
],
"id":1
},
{
"jsonrpc":"2.0",
"method":"subtract",
"params":[
42,
1
],
"id":2
}
]val subtract: JsonRpcServerEndpoint[Id] =
jsonRpcEndpoint(m"subtract")
.in(
param[Int]("subtrahend")
.and(param[Int]("minuend"))
)
.out[Int]("result of the subtraction")
.serverLogic[Id] { case (subtrahend, minuend) =>
Right(subtrahend - minuend)
}<-- [
{
"jsonrpc":"2.0",
"result":19,
"id":"1"
},
{
"jsonrpc":"2.0",
"result":41,
"id":"2"
}
]Interpreters - http
subtract: JsonRpcServerEndpoint[F[_]]sum: JsonRpcServerEndpoint[F[_]]greeting: JsonRpcServerEndpoint[F[_]]tapir
interperter
tapirEndpoint: ServerEndpoint[-R, F[_]]http4s
zio-http
netty
...
Endpoint[String, (Json, StatusCode), (Option[Json], StatusCode)]Interpreters - docs
subtract: JsonRpcServerEndpoint[F[_]]sum: JsonRpcServerEndpoint[F[_]]greeting: JsonRpcServerEndpoint[F[_]]OpenRpc
interperter
openRpcSpecification: Either[Json, Yaml]Interpreters
subtract: JsonRpcServerEndpoint[F[_]]sum: JsonRpcServerEndpoint[F[_]]greeting: JsonRpcServerEndpoint[F[_]]Fs2
interperter
jsonRpcServer: fs2.Pipe[IO, String, Json]unix-domain-socket
websocket
Extension points
Interceptor desing based on tapir's interceptors
Allow for customization on three different layers:
- request layer
- method layer
- endpoint
Armadillo
jsonRpcEndpoint
jsonRpcEndpoint
jsonRpcEndpoint
Custom Interceptor
legacyEndpoint
Bonus



https://www.flaticon.com/free-icon/armadillo_4215064

Demo
Thank you!
https://github.com/input-output-hk/armadillo
https://github.com/ghostbuster91

https://github.com/softwaremill/tapir
kkondzielski
armadillo - type safe json-rpc api
By Kasper Kondzielski
armadillo - type safe json-rpc api
- 244