JSON Application RPC, version 1.0 draft.
JARPC, pronounced jarpsee, stands for JSON Application RPC.
JARPC is a standard JSON RPC protocol optimized for web applications. It uses HTTP paths as procedure names, JSON request bodies for parameters, and a consistent response envelope for application results.
- Keep request and response bodies flat and predictable.
- Map naturally to HTTP routes and backend files.
- Work well with JavaScript
fetch(). - Work well with plain HTML forms and
FormData. - Avoid nested form conventions, dot-path parsing, and heavy serializers.
- Standardize common application concerns such as validation errors and pagination.
- Remain mechanically adaptable to JSON-RPC and MCP.
The HTTP path identifies the procedure.
POST /users/create
Content-Type: application/jsonRequest:
{
"id": "r-1001",
"params": {
"email": "ada@example.com",
"password": "secret"
}
}There is no body-level method. The route path is the method. The request method is usually POST, not GET.
type JARPCRequest<P = unknown> = {
id?: string | number
params?: P
}Fields:
id: optional request correlation identifier.params: endpoint input parameters.
If id is present, the response must echo it exactly. If id is omitted, the response should omit it.
For form-oriented endpoints, params should usually be a flat object whose keys match HTML input name attributes.
var params = Object.fromEntries(new FormData(form))JARPC does not define dot-notation or bracket-notation expansion for nested form values. Field names are opaque strings.
A successful response has ok: true.
{
"ok": true,
"id": "r-1001",
"data": {
"userId": 42
}
}Success with no returned data:
{
"ok": true,
"id": "r-1001"
}Fields:
ok: required, alwaystruefor success.id: echoed only when supplied by the request.data: primary successful return value.pagination: standardized pagination metadata, when applicable.details: endpoint-specific supplemental information, when documented.
An error response has ok: false.
{
"ok": false,
"id": "r-1001",
"message": "Unauthorized",
"code": "UNAUTHORIZED"
}Fields:
ok: required, alwaysfalsefor application failure.id: echoed only when supplied by the request.message: human-readable error message.code: optional stable machine-readable error code. It may be a string or number.fields: standardized field/input validation errors.details: endpoint-specific supplemental information, when documented.
Field validation errors must use fields.
{
"ok": false,
"id": "r-1001",
"message": "Validation failed",
"code": "VALIDATION_ERROR",
"fields": {
"email": ["Must be a valid email"],
"password": ["Must be at least 12 characters"]
}
}fields is a flat object whose keys are field names and whose values are arrays of validation messages.
Field names are opaque strings and should normally match HTML input name attributes.
<input name="email" />
<input name="company_name" />
<input name="items_0_name" />Example:
{
"fields": {
"company_name": ["Required"],
"items_0_name": ["Required"]
}
}JARPC does not define nested field syntax. It does not require clients to parse dot notation, bracket notation, or object paths.
Field errors must not be placed in details.
JARPC favors flat form flows.
For web application forms, deeply nested form payloads often require custom serializers, path parsers, deep merging, and fragile error mapping. JARPC avoids prescribing those mechanisms.
A simple form can be serialized with:
var params = Object.fromEntries(new FormData(form))And field errors can be mapped back with:
for (var [name, messages] of Object.entries(result.fields || {})) {
var input = form.elements.namedItem(name)
showFieldError(input, messages)
}Applications should prefer flat field names and, when useful, multi-step flows over deeply nested form payloads.
Paginated responses must use pagination.
Page-based pagination:
{
"ok": true,
"data": [
{
"id": 1,
"name": "Ada"
}
],
"pagination": {
"page": 1,
"pageSize": 20,
"total": 134,
"hasMore": true
}
}Cursor-based pagination:
{
"ok": true,
"data": [
{
"id": 1,
"name": "Ada"
}
],
"pagination": {
"nextCursor": "eyJpZCI6MX0=",
"previousCursor": null,
"hasMore": true
}
}Pagination metadata must not be placed in details.
Suggested type:
type Pagination =
| {
page: number
pageSize: number
total?: number
hasMore?: boolean
}
| {
nextCursor?: string | null
previousCursor?: string | null
hasMore: boolean
}details is an endpoint-specific supplemental field.
{
"ok": false,
"message": "Import failed",
"code": "IMPORT_FAILED",
"details": {
"row": 17,
"column": "email",
"rawValue": "not-an-email"
}
}Rules:
detailshas no global meaning.- Each endpoint that returns
detailsmust document its structure and meaning. - Do not use
detailsfor standardized concepts such asfieldsorpagination.
JARPC separates transport status from application status.
Application results are represented by the JSON envelope:
{
"ok": false,
"message": "User not found",
"code": "USER_NOT_FOUND"
}This may be returned with HTTP 200, because the endpoint executed successfully and returned an application-level result.
Recommended HTTP status usage:
200: endpoint executed and returned a JARPC result, whetherokis true or false.404: endpoint/path does not exist.500: server crash or unexpected infrastructure failure.429: request throttled before normal endpoint execution.301/302: HTTP-level redirect.401/403: HTTP-level auth only, such as Basic Auth, proxy auth, or middleware rejection.
Application authentication and authorization errors should normally use HTTP 200 with ok: false:
{
"ok": false,
"message": "Please sign in",
"code": "UNAUTHENTICATED"
}{
"ok": false,
"message": "You do not have permission to do that",
"code": "UNAUTHORIZED"
}var response = await fetch('/users/create', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
id: 'r-1001',
params: Object.fromEntries(new FormData(form))
})
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
var result = await response.json()
if (!result.ok) {
displayErrors(result)
return
}
render(result.data)There are two levels of success:
response.ok // HTTP/transport success
result.ok // application successtype JARPCId = string | number
type FieldErrors = Record<string, string[]>
type Pagination =
| {
page: number
pageSize: number
total?: number
hasMore?: boolean
}
| {
nextCursor?: string | null
previousCursor?: string | null
hasMore: boolean
}
type JARPCRequest<P = unknown> = {
id?: JARPCId
params?: P
}
type JARPCSuccess<T = unknown, D = unknown> = {
ok: true
id?: JARPCId
data?: T
pagination?: Pagination
details?: D
}
type JARPCFailure<D = unknown> = {
ok: false
id?: JARPCId
message: string
code?: string | number
fields?: FieldErrors
details?: D
}
type JARPCResponse<T = unknown, D = unknown> =
JARPCSuccess<T, D> | JARPCFailure<D>JARPC can be adapted to JSON-RPC by treating the HTTP path as the JSON-RPC method.
JARPC request:
POST /users/create{
"id": "r-1001",
"params": {
"email": "ada@example.com"
}
}JSON-RPC request:
{
"jsonrpc": "2.0",
"id": "r-1001",
"method": "users/create",
"params": {
"email": "ada@example.com"
}
}JARPC success:
{
"ok": true,
"id": "r-1001",
"data": {
"userId": 42
}
}JSON-RPC success:
{
"jsonrpc": "2.0",
"id": "r-1001",
"result": {
"data": {
"userId": 42
}
}
}JARPC validation error:
{
"ok": false,
"id": "r-1001",
"message": "Validation failed",
"code": "VALIDATION_ERROR",
"fields": {
"email": ["Must be a valid email"]
}
}JSON-RPC error:
{
"jsonrpc": "2.0",
"id": "r-1001",
"error": {
"code": -32602,
"message": "Validation failed",
"data": {
"appCode": "VALIDATION_ERROR",
"fields": {
"email": ["Must be a valid email"]
}
}
}
}JARPC is therefore suitable as a native web application protocol, while still being mechanically adaptable to JSON-RPC-based systems such as MCP.
Created by Vidar Eldøy