JSON-RPC Tutorials for Beginners

Introduction to JSON-RPC

JSON-RPC (JavaScript Object Notation Remote Procedure Call) is a lightweight remote procedure call protocol that uses JSON for data encoding. It is designed to be simple, transport agnostic, and easy to implement in any programming language that supports JSON parsing and generation.

This protocol allows clients to invoke methods on remote systems by sending JSON-formatted requests. The server processes these requests and returns results or error notifications in a standardized JSON format. JSON-RPC can be used over various transport protocols such as HTTP, WebSockets, TCP, or even in-process communication.

Why Use JSON-RPC?

  • Simplicity: Easy to understand and implement compared to more complex protocols
  • Language Agnostic: Can be used with any programming language that supports JSON
  • Lightweight: Minimal overhead compared to protocols like SOAP
  • Flexibility: Can be used over various transport mechanisms
  • Standardized: Well-defined specification makes implementations consistent

There are two versions of JSON-RPC: 1.0 and 2.0. This tutorial series focuses on JSON-RPC 2.0, which addresses limitations in the original version and provides a more robust foundation for remote procedure calls.

JSON-RPC Basics

At its core, JSON-RPC involves a client sending a request to a server, and the server sending back a response. Both the request and response are formatted as JSON objects with specific fields.

Key Components of JSON-RPC

  • Request Object: Contains the method name, parameters, and an identifier
  • Response Object: Contains the result or error and the same identifier as the request
  • Notification: A request without an ID that doesn't require a response
  • Batch Processing: Multiple requests/notifications sent together

JSON-RPC 2.0 Request Format

{
  "jsonrpc": "2.0",     // Protocol version - always "2.0" for JSON-RPC 2.0
  "method": "subtract", // Name of the method to invoke
  "params": [42, 23],   // Parameters for the method (can be array or object)
  "id": 1               // Request identifier (string or number)
}

The params field can be either an array (for positional parameters) or an object (for named parameters). For example, the same request with named parameters would look like:

{
  "jsonrpc": "2.0",
  "method": "subtract",
  "params": { "minuend": 42, "subtrahend": 23 },
  "id": 1
}

JSON-RPC 2.0 Response Format

A successful response includes:

{
  "jsonrpc": "2.0", // Protocol version
  "result": 19,     // The result of the method execution
  "id": 1           // Same id as in the request
}

An error response includes:

{
  "jsonrpc": "2.0",
  "error": {
    "code": -32602,       // Error code
    "message": "Invalid params", // Human-readable message
    "data": "Additional error details" // Optional field with additional info
  },
  "id": 1
}

Creating JSON-RPC Requests

Creating a valid JSON-RPC request involves ensuring all required fields are present and formatted correctly.

Required Fields

  • jsonrpc: Must be exactly "2.0" to indicate JSON-RPC 2.0
  • method: A string containing the name of the method to be invoked
  • params: (Optional) An array or object of parameter values
  • id: A string or number to identify the request (omit for notifications)

Example Requests

Simple method with no parameters:

{
  "jsonrpc": "2.0",
  "method": "getServerTime",
  "id": 1
}

Method with positional parameters:

{
  "jsonrpc": "2.0",
  "method": "sum",
  "params": [1, 2, 3, 4, 5],
  "id": 2
}

Method with named parameters:

{
  "jsonrpc": "2.0",
  "method": "createUser",
  "params": {
    "username": "johndoe",
    "email": "[email protected]",
    "firstName": "John",
    "lastName": "Doe"
  },
  "id": 3
}

JSON-RPC Notifications

Notifications are requests that don't require a response. They're identified by the absence of an "id" field. The server should not reply to notifications, even if an error occurs.

{
  "jsonrpc": "2.0",
  "method": "logEvent",
  "params": ["User logged in", "INFO"]
}

Note

Since notifications don't have an ID, the client cannot match responses to notifications. Use notifications only when you don't care about the result or potential errors.

Understanding Responses

When a JSON-RPC server processes a request, it must respond with a properly formatted response object. Understanding how to parse and handle these responses is crucial for implementing client applications.

Success Response Structure

A successful response must contain these fields:

  • jsonrpc: Must be exactly "2.0"
  • result: The value returned by the method (any JSON value is allowed)
  • id: Must match the id of the corresponding request

Examples of successful responses:

{
  "jsonrpc": "2.0",
  "result": 19,
  "id": 1
}

A response with a more complex result:

{
  "jsonrpc": "2.0",
  "result": {
    "userId": 123,
    "username": "johndoe",
    "email": "[email protected]",
    "createdAt": "2023-01-15T08:30:00Z"
  },
  "id": 3
}

If the result is null, it should be included explicitly:

{
  "jsonrpc": "2.0",
  "result": null,
  "id": 4
}

Error Handling

When a JSON-RPC request cannot be processed successfully, the server should return an error response. Proper error handling is essential for robust client and server implementations.

Error Response Structure

An error response must contain these fields:

  • jsonrpc: Must be exactly "2.0"
  • error: An object describing the error that occurred
  • id: Must match the id of the corresponding request, or null if the id couldn't be determined

The error object must contain:

  • code: A number indicating the error type
  • message: A short description of the error
  • data: (Optional) Additional information about the error

Standard Error Codes

The JSON-RPC 2.0 specification defines these standard error codes:

CodeMessageDescription
-32700Parse errorInvalid JSON was received
-32600Invalid RequestThe JSON sent is not a valid Request object
-32601Method not foundThe method does not exist / is not available
-32602Invalid paramsInvalid method parameter(s)
-32603Internal errorInternal JSON-RPC error
-32000 to -32099Server errorReserved for implementation-defined server errors

Example of an error response:

{ "jsonrpc": "2.0", "error": { "code": -32601, "message": "Method not found", "data": "The method 'calculateTax' does not exist" }, "id": 5 }

Batch Processing

JSON-RPC 2.0 allows multiple requests to be sent in a single batch, which can significantly reduce overhead when making multiple calls.

Batch Request Format

A batch request is simply an array of individual request objects:

[
  {"jsonrpc": "2.0", "method": "getUser", "params": [1], "id": 1},
  {"jsonrpc": "2.0", "method": "getPermissions", "params": [1], "id": 2},
  {"jsonrpc": "2.0", "method": "logAccess", "params": ["user profile viewed"]}
]

In this example, we're sending two requests (with IDs 1 and 2) and one notification (without an ID).

Batch Response Format

The server responds with an array of response objects, one for each request that had an ID:

[
  {"jsonrpc": "2.0", "result": {"id": 1, "name": "John Doe"}, "id": 1},
  {"jsonrpc": "2.0", "result": ["read", "edit", "delete"], "id": 2}
]

Note that there's no response for the notification (the third request in our batch).

Handling Errors in Batch Requests

If one request in a batch fails, only that specific request will have an error response. The other requests will still be processed normally:

[
  {"jsonrpc": "2.0", "result": {"id": 1, "name": "John Doe"}, "id": 1},
  {"jsonrpc": "2.0", "error": {"code": -32601, "message": "Method not found"}, "id": 2}
]

Important Note

If the batch request itself is not a valid JSON array, the server should return a single error response rather than a batch response.

Server Implementation

Implementing a JSON-RPC server involves setting up an endpoint that can receive, parse, and respond to JSON-RPC requests. Here we'll explore a basic implementation using Node.js and Express.

Basic Server Example

A simple JSON-RPC server in Node.js using Express:

server.js
const express = require('express');
const bodyParser = require('body-parser');
const app = express();

// Middleware to parse JSON requests
app.use(bodyParser.json());

// Available methods
const methods = {
  add: (params) => {
    if (Array.isArray(params)) {
      return params.reduce((sum, val) => sum + val, 0);
    } else {
      return params.a + params.b;
    }
  },
  subtract: (params) => {
    if (Array.isArray(params)) {
      return params[0] - params[1];
    } else {
      return params.minuend - params.subtrahend;
    }
  },
  getTime: () => {
    return new Date().toISOString();
  }
};

// JSON-RPC endpoint
app.post('/rpc', (req, res) => {
  const request = req.body;
  let response;
  
  // Check if it's a batch request
  if (Array.isArray(request)) {
    response = request.map(handleSingleRequest).filter(r => r !== null);
  } else {
    response = handleSingleRequest(request);
  }
  
  res.json(response);
});

function handleSingleRequest(request) {
  // Check for valid JSON-RPC 2.0 request
  if (request.jsonrpc !== '2.0' || !request.method) {
    return {
      jsonrpc: '2.0',
      error: {
        code: -32600,
        message: 'Invalid Request'
      },
      id: request.id || null
    };
  }
  
  // Check if method exists
  if (!methods[request.method]) {
    return {
      jsonrpc: '2.0',
      error: {
        code: -32601,
        message: 'Method not found'
      },
      id: request.id
    };
  }
  
  try {
    // Execute the method
    const result = methods[request.method](request.params);
    
    // Don't return a response for notifications
    if (request.id === undefined) {
      return null;
    }
    
    // Return the result
    return {
      jsonrpc: '2.0',
      result,
      id: request.id
    };
  } catch (e) {
    // Return an error
    return {
      jsonrpc: '2.0',
      error: {
        code: -32603,
        message: 'Internal error',
        data: e.message
      },
      id: request.id
    };
  }
}

app.listen(3000, () => {
  console.log('JSON-RPC server running on port 3000');
});

Key Server Implementation Considerations

  • Method Registration: Define a clear way to register available methods
  • Input Validation: Validate request parameters before execution
  • Error Handling: Implement proper error handling and return appropriate error codes
  • Authentication: Add authentication layers if needed
  • Notifications: Handle notifications properly (no response needed)
  • Batch Processing: Support for multiple requests in a single call

Client Implementation

Implementing a JSON-RPC client involves sending properly formatted requests and handling the responses. Let's look at a JavaScript example using the Fetch API.

Basic Client Example

client.js
class JsonRpcClient {
  constructor(endpoint) {
    this.endpoint = endpoint;
    this.id = 1;
  }
  
  async call(method, params) {
    const request = {
      jsonrpc: '2.0',
      method,
      params,
      id: this.id++
    };
    
    try {
      const response = await fetch(this.endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(request)
      });
      
      const result = await response.json();
      
      if (result.error) {
        throw new Error(`JSON-RPC Error [${result.error.code}]: ${result.error.message}`);
      }
      
      return result.result;
    } catch (error) {
      console.error('JSON-RPC call failed:', error);
      throw error;
    }
  }
  
  // Method for sending notifications (no response expected)
  notify(method, params) {
    const request = {
      jsonrpc: '2.0',
      method,
      params
    };
    
    fetch(this.endpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(request)
    }).catch(error => {
      console.error('JSON-RPC notification failed:', error);
    });
  }
  
  // Method for batch requests
  async batch(calls) {
    const requests = calls.map(call => {
      if (call.notification) {
        return {
          jsonrpc: '2.0',
          method: call.method,
          params: call.params
        };
      } else {
        return {
          jsonrpc: '2.0',
          method: call.method,
          params: call.params,
          id: this.id++
        };
      }
    });
    
    try {
      const response = await fetch(this.endpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(requests)
      });
      
      const results = await response.json();
      
      // Map results back to their corresponding calls
      return results.map(result => {
        if (result.error) {
          throw new Error(`JSON-RPC Error [${result.error.code}]: ${result.error.message}`);
        }
        return result.result;
      });
    } catch (error) {
      console.error('JSON-RPC batch call failed:', error);
      throw error;
    }
  }
}

// Example usage:
async function example() {
  const client = new JsonRpcClient('http://localhost:3000/rpc');
  
  try {
    // Simple call
    const sum = await client.call('add', [1, 2, 3, 4, 5]);
    console.log('Sum result:', sum);
    
    // Call with named parameters
    const diff = await client.call('subtract', { minuend: 42, subtrahend: 23 });
    console.log('Subtraction result:', diff);
    
    // Notification (no response)
    client.notify('logEvent', ['user action', 'clicked button']);
    
    // Batch request
    const batchResults = await client.batch([
      { method: 'add', params: [1, 2] },
      { method: 'getTime', params: [] },
      { method: 'logEvent', params: ['batch event'], notification: true }
    ]);
    
    console.log('Batch results:', batchResults);
  } catch (error) {
    console.error('Error:', error.message);
  }
}

example();

Key Client Implementation Considerations

  • Request IDs: Generate unique IDs for each request to match responses
  • Error Handling: Properly handle and report errors from the server
  • Timeout Handling: Implement timeouts for requests
  • Retry Logic: Add retry mechanisms for failed requests
  • Authentication: Include authentication tokens if required
  • Notifications: Support for fire-and-forget requests

Security Considerations

When implementing JSON-RPC services, it's important to be aware of potential security risks and how to mitigate them. Here are key security considerations for your JSON-RPC APIs:

Common Security Risks

  • Unauthorized Access: Without proper authentication, attackers may invoke methods they shouldn't have access to.
  • Injection Attacks: If parameters aren't properly validated, they may contain malicious code or SQL injection attempts.
  • Request Forgery: Cross-Site Request Forgery (CSRF) attacks can trick authenticated users into making unwanted RPC calls.
  • Information Disclosure: Error responses might leak sensitive implementation details or data.
  • Denial of Service: Complex requests or batch processing can be used to consume excessive server resources.

Security Best Practices

1. Use HTTPS

Always use HTTPS to encrypt data in transit. This prevents man-in-the-middle attacks and eavesdropping.

2. Implement Authentication

Use industry-standard authentication methods like JWT, OAuth, or API keys. Add the authentication token in HTTP headers rather than in the JSON-RPC request body.

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

3. Validate All Inputs

Thoroughly validate method names and parameters. Use schema validation to ensure parameters match expected types and ranges.

4. Implement Method-Level Authorization

Even after authentication, verify that the authenticated user has permission to call the specific method with the provided parameters.

5. Set Request Size Limits

Limit the size of incoming requests and the number of operations in batch requests to prevent denial of service attacks.

6. Implement Rate Limiting

Protect your API from abuse by limiting the number of requests a client can make in a given time period.

7. Use Safe Error Responses

Be cautious about the information included in error messages. Don't leak implementation details, stack traces, or sensitive data in error responses.

Security Warning

Never blindly execute method names or parameters received from clients. Always validate and sanitize all inputs, and maintain a whitelist of allowed methods. Don't use eval() or similar dynamic execution mechanisms with client inputs.

Want to see the complete tutorial?

We are continuously adding more detailed content to our tutorials.

Check the JSON-RPC 2.0 Specification

Next Steps

After mastering the basics of JSON-RPC, you might want to explore more advanced topics:

  • Implement authentication for your JSON-RPC APIs
  • Explore version control strategies for your API methods
  • Implement advanced error handling and logging
  • Create client libraries for your JSON-RPC services
  • Explore JSON-RPC over different transport protocols

Be sure to check out our tools to help with JSON-RPC development: