JSON-RPC 2.0: Request/Response Structure, Error Codes & JavaScript Implementation

JSON-RPC 2.0 is a stateless, transport-agnostic remote procedure call protocol that uses JSON to encode requests and responses. A request sends {"jsonrpc":"2.0","method":"add","params":[1,2],"id":1} and receives {"jsonrpc":"2.0","result":3,"id":1}. There are 3 call styles: positional params (array), named params (object), and notifications (no id field — no response expected). Batch requests send an array of request objects and receive an array of response objects. This guide covers the full JSON-RPC 2.0 request/response structure, 5 standard error codes, notifications, batch calls, implementing a server in Node.js and Python, and comparing JSON-RPC with REST and GraphQL. Use Jsonic's JSON formatter to validate and prettify JSON-RPC payloads during development.

Need to inspect or validate a JSON-RPC payload? Jsonic's JSON formatter prettifies and validates any JSON object instantly.

Open JSON Formatter

JSON-RPC 2.0 request and response structure

Every JSON-RPC 2.0 request has exactly 4 fields. The spec (published in 2010) is intentionally minimal: "jsonrpc" must always be the string "2.0" — omitting it means JSON-RPC 1.0 with different semantics. "method" is the name of the procedure to call. "params" is optional and can be either an array (positional) or an object (named). "id" identifies the call so the client can match the response; omitting it makes the request a notification.

// Positional params (array)
{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}

// Named params (object)
{"jsonrpc": "2.0", "method": "add", "params": {"a": 1, "b": 2}, "id": 2}

// Notification — no "id" field, server must not respond
{"jsonrpc": "2.0", "method": "log.event", "params": {"level": "info", "msg": "started"}}

A successful response has 3 fields: "jsonrpc":"2.0", "result" (the return value — any valid JSON), and "id" matching the request. An error response replaces "result" with "error" — an object containing "code" (integer), "message" (string), and an optional "data" field. The "result" and "error" fields are mutually exclusive.

// Success response
{"jsonrpc": "2.0", "result": 3, "id": 1}

// Error response
{
  "jsonrpc": "2.0",
  "error": {"code": -32601, "message": "Method not found"},
  "id": 1
}

JSON-RPC 2.0 error codes

The spec defines 5 standard error codes. All standard codes fall in the reserved range -32768 to -32000. Application-specific codes must be outside this range. The "message" field is a short human-readable description; the optional "data" field carries additional structured detail (stack trace, validation errors, etc.).

CodeMessageMeaning
-32700Parse errorInvalid JSON received — the server could not parse the request body at all
-32600Invalid RequestValid JSON but not a valid JSON-RPC 2.0 request object (missing "method", wrong "jsonrpc" value, etc.)
-32601Method not foundThe method named in "method" does not exist on the server
-32602Invalid paramsMethod exists but params are wrong type, missing required param, or extra params when none expected
-32603Internal errorServer-side error during method execution — params were valid but the call failed internally
-32000 to -32099(implementation-defined)Reserved for server-defined error conditions — document these in your API

Error responses always include "id" matching the request (or null if the id could not be determined, e.g. on a parse error). Never send an error response to a notification — even if the notification caused a server error. Here is how to construct an error response in JavaScript:

function errorResponse(id, code, message, data) {
  return {
    jsonrpc: '2.0',
    error: { code, message, ...(data !== undefined && { data }) },
    id,
  }
}

// Method not found
errorResponse(1, -32601, 'Method not found')
// { jsonrpc: '2.0', error: { code: -32601, message: 'Method not found' }, id: 1 }

// Invalid params with detail
errorResponse(2, -32602, 'Invalid params', { missing: ['b'] })
// { jsonrpc: '2.0', error: { code: -32602, message: 'Invalid params', data: { missing: ['b'] } }, id: 2 }

Use Jsonic's formatter to inspect error response objects during debugging — paste the raw response to see it pretty-printed with all fields visible.

Notifications and batch requests

Notifications are requests without an "id" field. The server processes them but must not send any response — not even an error. This makes notifications fire-and-forget: use them for logging, telemetry, or side effects where the client does not need confirmation. The Language Server Protocol uses notifications for 12+ message types, including textDocument/didChange, textDocument/didOpen, and $/cancelRequest.

// Notification — server must NOT respond
{"jsonrpc": "2.0", "method": "textDocument/didChange", "params": {
  "textDocument": {"uri": "file:///app.js", "version": 2},
  "contentChanges": [{"text": "const x = 1"}]
}}

Batch requests send an array of request objects in a single call. The server returns an array of response objects — 1 per request that has an "id". Notifications in the batch receive no response entry. Response order is not guaranteed — always match responses to requests using the "id" field. An empty batch array ([]) is an invalid request and returns a single -32600 error.

// Batch request — 2 calls + 1 notification
[
  {"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1},
  {"jsonrpc": "2.0", "method": "multiply", "params": [3, 4], "id": 2},
  {"jsonrpc": "2.0", "method": "log.event", "params": {"msg": "batch sent"}}
]

// Batch response — 2 results (no entry for the notification)
[
  {"jsonrpc": "2.0", "result": 3, "id": 1},
  {"jsonrpc": "2.0", "result": 12, "id": 2}
]

Batch requests reduce round-trips dramatically. Instead of 10 sequential HTTP POST calls (each with its own TCP overhead), send 1 batch and receive 1 response. This is especially valuable in Ethereum JSON-RPC clients where a single page load may need dozens of eth_call results — batching them cuts latency by 5–10x on typical connections.

Implement a JSON-RPC 2.0 server in Node.js

A JSON-RPC server over HTTP POST needs to: parse the body, validate it is a valid request or batch, dispatch to the named method, and return the response. The implementation below handles single requests, batch requests, notifications, and all 5 standard error codes in under 60 lines — no framework required.

import http from 'http'

// Method registry
const methods = {
  add: ({ a, b }) => a + b,
  subtract: ([a, b]) => a - b,
  greet: ({ name }) => `Hello, ${name}!`,
}

function handleRequest(req) {
  if (typeof req !== 'object' || req === null ||
      req.jsonrpc !== '2.0' || typeof req.method !== 'string') {
    return { jsonrpc: '2.0', error: { code: -32600, message: 'Invalid Request' }, id: req?.id ?? null }
  }
  const fn = methods[req.method]
  if (!fn) {
    return req.id !== undefined
      ? { jsonrpc: '2.0', error: { code: -32601, message: 'Method not found' }, id: req.id }
      : null  // notification — no response
  }
  try {
    const result = fn(req.params ?? [])
    return req.id !== undefined
      ? { jsonrpc: '2.0', result, id: req.id }
      : null  // notification — no response
  } catch (e) {
    return req.id !== undefined
      ? { jsonrpc: '2.0', error: { code: -32603, message: 'Internal error', data: e.message }, id: req.id }
      : null
  }
}

const server = http.createServer(async (req, res) => {
  if (req.method !== 'POST') { res.writeHead(405); res.end(); return }
  let body = ''
  for await (const chunk of req) body += chunk
  let parsed
  try { parsed = JSON.parse(body) } catch {
    res.writeHead(200, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null }))
    return
  }
  let response
  if (Array.isArray(parsed)) {
    const results = parsed.map(handleRequest).filter(r => r !== null)
    response = results.length > 0 ? results : null
  } else {
    response = handleRequest(parsed)
  }
  res.writeHead(200, { 'Content-Type': 'application/json' })
  res.end(response !== null ? JSON.stringify(response) : '')
})

server.listen(3000, () => console.log('JSON-RPC server on :3000'))

Test with curl — single call and batch:

# Single call
curl -s -X POST http://localhost:3000 \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","method":"add","params":{"a":3,"b":4},"id":1}'
# {"jsonrpc":"2.0","result":7,"id":1}

# Batch
curl -s -X POST http://localhost:3000 \
  -H 'Content-Type: application/json' \
  -d '[{"jsonrpc":"2.0","method":"add","params":{"a":1,"b":2},"id":1},
       {"jsonrpc":"2.0","method":"greet","params":{"name":"World"},"id":2}]'
# [{"jsonrpc":"2.0","result":3,"id":1},{"jsonrpc":"2.0","result":"Hello, World!","id":2}]

For production use, consider the jayson npm package (MIT license), which provides a full JSON-RPC 2.0 server and client for HTTP, TCP, and WebSocket with middleware support. See the fetch JSON in JavaScript guide for building the client side.

Implement a JSON-RPC 2.0 server in Python

Python's standard library handles JSON-RPC cleanly. The example below uses http.server and processes single requests, batches, and notifications following the same dispatch pattern as the Node.js version. For production, the jsonrpcserver pip package (MIT) provides decorators and WSGI/ASGI support.

from http.server import BaseHTTPRequestHandler, HTTPServer
import json

METHODS = {
    'add': lambda p: p['a'] + p['b'] if isinstance(p, dict) else p[0] + p[1],
    'subtract': lambda p: p[0] - p[1],
    'greet': lambda p: f"Hello, {p['name']}!",
}

def handle_single(req):
    if not isinstance(req, dict) or req.get('jsonrpc') != '2.0' or 'method' not in req:
        return {'jsonrpc': '2.0', 'error': {'code': -32600, 'message': 'Invalid Request'}, 'id': req.get('id')}
    fn = METHODS.get(req['method'])
    if fn is None:
        return {'jsonrpc': '2.0', 'error': {'code': -32601, 'message': 'Method not found'}, 'id': req.get('id')}             if 'id' in req else None
    try:
        result = fn(req.get('params', []))
        return {'jsonrpc': '2.0', 'result': result, 'id': req['id']} if 'id' in req else None
    except Exception as e:
        return {'jsonrpc': '2.0', 'error': {'code': -32603, 'message': str(e)}, 'id': req.get('id')}             if 'id' in req else None

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(length)
        try:
            parsed = json.loads(body)
        except json.JSONDecodeError:
            self._send({'jsonrpc': '2.0', 'error': {'code': -32700, 'message': 'Parse error'}, 'id': None})
            return
        if isinstance(parsed, list):
            results = [r for r in (handle_single(r) for r in parsed) if r is not None]
            self._send(results if results else None)
        else:
            self._send(handle_single(parsed))

    def _send(self, data):
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(json.dumps(data).encode() if data is not None else b'')

    def log_message(self, *_): pass  # suppress default logging

HTTPServer(('', 8000), Handler).serve_forever()

Install jsonrpcserver for a cleaner async implementation with FastAPI/Starlette:

pip install jsonrpcserver fastapi uvicorn
from fastapi import FastAPI, Request
from jsonrpcserver import method, async_dispatch

app = FastAPI()

@method
async def add(a: int, b: int) -> int:
    return a + b

@app.post('/rpc')
async def rpc(request: Request):
    return await async_dispatch(await request.body())

JSON-RPC vs REST vs GraphQL — when to use each

All 3 are popular API styles that use JSON, but they have fundamentally different design philosophies. Choosing the right one depends on your use case: resource CRUD operations, action-oriented APIs, or flexible data-fetching clients.

JSON-RPC 2.0RESTGraphQL
ModelProcedure callsResources + HTTP verbsGraph queries + mutations
TransportAny (HTTP, WS, stdio)HTTP onlyHTTP (typically POST)
BatchingBuilt-in (array)Not standardizedBuilt-in (multiple ops)
NotificationsBuilt-inNot applicableSubscriptions (WS)
Spec size~8 pagesArchitectural style (no wire spec)Large formal spec
Error handlingStandardized codesHTTP status codes"errors" array in response
Best forInternal RPC, LSP, blockchain, bidirectionalPublic APIs, CRUD resourcesComplex client-driven queries

Use JSON-RPC when you are building an internal service-to-service API with named operations, need bidirectional communication over WebSocket or stdio, or are implementing a protocol like LSP. Use REST for public APIs exposing CRUD resources over HTTP — the resource model and HTTP semantics are widely understood and tooled. Use GraphQL when clients need to request exactly the fields they want across a complex object graph, reducing over-fetching. JSON:API is a fourth option — a REST-based spec for resource APIs that standardizes pagination, relationships, and filtering.

JSON-RPC in the real world: LSP, Ethereum, and more

JSON-RPC 2.0 powers several of the most widely-used developer tools and protocols. Understanding how each uses the spec reveals why JSON-RPC's transport-agnostic design was the right choice.

Language Server Protocol (LSP) — used by VS Code, Neovim, Emacs, and every modern editor — is built entirely on JSON-RPC 2.0 over stdio or TCP. The editor (client) sends requests like textDocument/completion (with id — expects response) and notifications like textDocument/didChange (no id — fire and forget). The language server responds to requests and can also send its own notifications to the editor: textDocument/publishDiagnostics (error squiggles). LSP uses 40+ defined methods, all encoded as JSON-RPC.

// LSP: editor requests completions (request with id)
{"jsonrpc":"2.0","method":"textDocument/completion","params":{
  "textDocument":{"uri":"file:///src/app.ts"},
  "position":{"line":10,"character":5}
},"id":42}

// LSP: language server publishes diagnostics (notification, no id)
{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{
  "uri":"file:///src/app.ts",
  "diagnostics":[{"range":{"start":{"line":2,"character":0},"end":{"line":2,"character":5}},
  "severity":1,"message":"Cannot find name 'foo'."}]
}}

Ethereum JSON-RPC — all Ethereum nodes (Geth, Besu, Infura, Alchemy) expose a JSON-RPC 2.0 API over HTTP and WebSocket. Every blockchain read and write is a JSON-RPC call. Applications use eth_getBalance, eth_sendRawTransaction, eth_call, and eth_subscribe (WebSocket notification subscription). The Ethereum JSON-RPC spec defines 30+ methods. Batching is critical here — a DeFi UI may batch 20 eth_call requests to get token balances in a single round-trip. You can stringify and inspect these payloads with Jsonic.

Other users: Bitcoin Core RPC (JSON-RPC 1.1), Apache Kafka admin API, VS Code extension host, TypeScript language service, clangd (C++ LSP), rust-analyzer — all use JSON-RPC as their internal or external wire protocol. The common thread: all needed a lightweight, debuggable RPC protocol that could run over a simple bidirectional byte stream.

Frequently asked questions

What is JSON-RPC 2.0 and how does it differ from REST?

JSON-RPC 2.0 is a stateless remote procedure call protocol that uses JSON for encoding, published in 2010. It defines a minimal wire format: a request object with a method name, optional params, and an id; a response with either a result or an error. Unlike REST, which models APIs as resources accessed via HTTP verbs (GET /users/1, DELETE /users/1), JSON-RPC models APIs as function calls: {"jsonrpc":"2.0","method":"users.delete","params":{"id":1},"id":1}. REST is resource-oriented and uses HTTP semantics (status codes, verbs, headers) to convey meaning. JSON-RPC is transport-agnostic — the same protocol runs over HTTP POST, WebSocket, TCP, or stdio. There is no concept of URL structure, HTTP verbs, or status codes in the JSON-RPC spec. This makes JSON-RPC simpler when you want to expose a fixed set of named operations, and more natural for bidirectional or streaming transports like WebSocket. See the REST API JSON response guide for the contrasting REST approach.

What is the structure of a JSON-RPC 2.0 request and response?

A JSON-RPC 2.0 request has 4 fields: "jsonrpc" (always the string "2.0"), "method" (the procedure name), "params" (optional array or object), and "id" (string, number, or null — omitting it makes the request a notification). A successful response has "jsonrpc":"2.0", "result" (any JSON value), and "id" matching the request. An error response has "jsonrpc":"2.0", "error" (object with "code", "message", and optional "data"), and "id". The "result" and "error" fields are mutually exclusive — a response has exactly one of them. The "id" must be null if the request id could not be determined (e.g. on a parse error). Omitting "jsonrpc":"2.0" means you are sending JSON-RPC 1.0, which has different semantics — always include it. Use Jsonic to validate and prettify your request and response objects.

What is a JSON-RPC notification and when do I use it?

A JSON-RPC notification is a request object without an "id" field. When the server receives a notification, it must not send any response — not even an error response. Notifications are one-way fire-and-forget messages. Use notifications when the client does not need confirmation that the call succeeded: logging an event, sending a telemetry ping, pushing a status update, or triggering a side effect where the result does not matter. The Language Server Protocol (LSP) uses notifications for 12+ message types — for example, textDocument/didChange notifies the language server that the user edited a file; the client does not wait for a response. $/cancelRequest cancels an in-flight request and is also a notification. Do not use notifications when you need to know whether the call succeeded or what it returned. If the server encounters an error processing a notification (including a parse error on a pure notification), it must silently discard it — no error response is ever sent for a notification.

How do JSON-RPC batch requests work?

A JSON-RPC batch request sends an array of request objects in a single transport call. The server processes all requests in the batch (potentially in parallel) and returns an array of response objects — one per request that had an "id". Notifications in the batch receive no response entry, so the response array may be shorter than the request array. The order of responses is not guaranteed to match the order of requests — clients must match responses to requests using the "id" field. If the batch array is empty ([]), the server returns a single -32600 Invalid Request error. Batch requests are valuable for reducing round-trips: instead of making 10 separate HTTP calls, send 1 batch and receive 1 response. Ethereum DeFi frontends use batching heavily — a single page load batches 20+ eth_call requests to fetch token balances in one round-trip. Not all JSON-RPC implementations support batching — always check the server documentation. You can fetch JSON from APIs and handle batch responses using the same id-matching logic.

What are the JSON-RPC 2.0 error codes and what do they mean?

JSON-RPC 2.0 defines 5 standard error codes. -32700 (Parse error): the JSON sent is not valid JSON — the server could not parse the request body. -32600 (Invalid Request): the JSON is valid but is not a valid JSON-RPC 2.0 request — missing "method", wrong "jsonrpc" value, or invalid "id" type. -32601 (Method not found): the method named in "method" does not exist on the server. -32602 (Invalid params): the method exists but the parameters are invalid — wrong type, missing required param, or extra params when the method expects none. -32603 (Internal error): a server-side error during method execution — params were valid but the call failed internally. Codes from -32000 to -32099 are reserved for implementation-defined server errors; codes outside -32768 to -32000 are for application-specific errors. The optional "data" field in the error object can carry structured detail (stack trace, validation errors, field names) to help the client diagnose the problem. See the JSON stringify guide for serializing error details correctly.

What uses JSON-RPC in practice (LSP, Ethereum, etc.)?

JSON-RPC 2.0 is used in several prominent real-world systems. The Language Server Protocol (LSP), created by Microsoft and used by VS Code, Neovim, Emacs, and every modern editor, is built entirely on JSON-RPC 2.0 over stdio or TCP — all editor-to-language-server communication (go to definition, autocomplete, diagnostics) uses JSON-RPC messages. Ethereum nodes (Geth, Infura, Alchemy) expose a JSON-RPC API over HTTP and WebSocket — methods like eth_getBalance, eth_sendTransaction, and eth_call are all JSON-RPC calls; the Ethereum JSON-RPC spec defines 30+ methods. Bitcoin Core exposes a JSON-RPC 1.1 interface. Developer tools like TypeScript's language service, clangd (C++ LSP), and rust-analyzer all use JSON-RPC internally. The common factor: all needed a lightweight, debuggable, transport-agnostic RPC protocol over a bidirectional byte stream, where HTTP-specific semantics (status codes, verbs) would add unnecessary complexity. See the JSON:API specification guide for a contrasting REST-based approach to API design.

Ready to work with JSON-RPC?

Use Jsonic's JSON formatter to validate and prettify your JSON-RPC request and response objects. You can also use the JSON Diff tool to compare two response objects and spot differences instantly.

Open JSON Formatter