Skip to content

JSON-RPC Request Builder — JavaScript source

Build valid JSON-RPC 2.0 requests, notifications, success responses, and error responses, plus batch arrays. Validate message structure.

This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Pure JSON-RPC 2.0 builder - JavaScript showcase port.
//
// Language: JavaScript (ES module)
// CosMoDev polyglot showcase port of the `json-rpc-builder` tool, ported
// from src/lib/jsonRpc.ts (the canonical, live TypeScript lib).
// Display source - part of CosmoDev's polyglot tool pages (dev.cosmolabs.org).
//
// Behavior is identical to the TypeScript lib: same inputs -> same outputs.
// Builds JSON-RPC 2.0 requests, notifications, success/error responses, and
// batches. Pure data logic - never throws; every call returns an Outcome.

export const JSONRPC_VERSION = '2.0';

// Pre-defined messages for the error codes reserved by the spec
// (https://www.jsonrpc.org/specification#error_object, section 5.1).
// -32000 to -32099 is the server-defined "Server error" band.
export const STANDARD_ERRORS = {
  [-32700]: { message: 'Parse error' },
  [-32600]: { message: 'Invalid Request' },
  [-32601]: { message: 'Method not found' },
  [-32602]: { message: 'Invalid params' },
  [-32603]: { message: 'Internal error' },
  [-32000]: { message: 'Server error' },
};

/**
 * @typedef {Object} BuildOptions
 * @property {number} [indent] - spaces per nesting level; omitted/0 = compact.
 */

/**
 * @typedef {Object} Outcome
 * @property {boolean} ok       - whether the build succeeded.
 * @property {string} json      - the serialized message (empty string on failure).
 * @property {(string|null)} error - failure reason, or null on success.
 */

const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;

// JSON.stringify's third argument is the indent width. Both `0` and
// `undefined` produce compact output, so defaulting a missing indent to 0
// keeps the two call shapes (with and without opts) equivalent.
const safeStringify = (obj, indent) => JSON.stringify(obj, null, indent ?? 0);

/**
 * Build a JSON-RPC 2.0 Request object.
 *
 * A request carries an `id` that the server echoes back in its response,
 * making it a synchronous ask/reply pair (as opposed to a Notification).
 *
 * @param {string} method              - remote method name.
 * @param {*} [params]                 - structured params; omit to leave unset.
 * @param {string|number|null} [id=1]  - caller-chosen correlation id.
 * @param {BuildOptions} [opts]
 * @returns {Outcome}
 */
export function buildRequest(method, params, id = 1, opts = {}) {
  if (!isNonEmptyString(method)) {
    return { ok: false, json: '', error: 'method must be a non-empty string' };
  }
  const obj = { jsonrpc: JSONRPC_VERSION, method };
  // `undefined` means "no params field"; any other value (incl. null) is emitted.
  if (params !== undefined) obj.params = params;
  obj.id = id;
  return { ok: true, json: safeStringify(obj, opts.indent), error: null };
}

/**
 * Build a JSON-RPC 2.0 Notification.
 *
 * A notification is a fire-and-forget request: it carries no `id`, so the
 * server MUST NOT reply. Used for events/telemetry.
 *
 * @param {string} method
 * @param {*} [params]
 * @param {BuildOptions} [opts]
 * @returns {Outcome}
 */
export function buildNotification(method, params, opts = {}) {
  if (!isNonEmptyString(method)) {
    return { ok: false, json: '', error: 'method must be a non-empty string' };
  }
  const obj = { jsonrpc: JSONRPC_VERSION, method };
  if (params !== undefined) obj.params = params;
  return { ok: true, json: safeStringify(obj, opts.indent), error: null };
}

/**
 * Build a JSON-RPC 2.0 success Response.
 *
 * @param {string|number|null} id - echoes the request id this answers.
 * @param {*} result              - the return value.
 * @param {BuildOptions} [opts]
 * @returns {Outcome}
 */
export function buildSuccessResponse(id, result, opts = {}) {
  const obj = { jsonrpc: JSONRPC_VERSION, result, id };
  return { ok: true, json: safeStringify(obj, opts.indent), error: null };
}

/**
 * Build a JSON-RPC 2.0 error Response.
 *
 * The message falls back through a chain: explicit argument -> the standard
 * text for the given code -> the generic word "Error".
 *
 * @param {string|number|null} id - echoes the request id (null if undetectable).
 * @param {number} code           - application or reserved error code.
 * @param {string} [message]      - one-line human summary.
 * @param {*} [data]              - optional structured diagnostic detail.
 * @param {BuildOptions} [opts]
 * @returns {Outcome}
 */
export function buildErrorResponse(id, code, message, data, opts = {}) {
  const msg = message ?? STANDARD_ERRORS[code]?.message ?? 'Error';
  const error = { code, message: msg };
  if (data !== undefined) error.data = data;
  const obj = { jsonrpc: JSONRPC_VERSION, error, id };
  return { ok: true, json: safeStringify(obj, opts.indent), error: null };
}

/**
 * Serialize a batch of pre-built JSON-RPC messages.
 *
 * Per spec (section 6), a batch is an array of requests/notifications/responses
 * exchanged in one round-trip. The caller supplies already-built message
 * objects (not strings); we only wrap and serialize them.
 *
 * @param {unknown[]} messages - non-empty array of message objects.
 * @param {BuildOptions} [opts]
 * @returns {Outcome}
 */
export function buildBatch(messages, opts = {}) {
  if (!Array.isArray(messages) || messages.length === 0) {
    return { ok: false, json: '', error: 'batch must be a non-empty array' };
  }
  return { ok: true, json: safeStringify(messages, opts.indent), error: null };
}

/**
 * Lightweight structural check for a JSON-RPC 2.0 message.
 *
 * This is intentionally permissive about field VALUES (it does not recurse
 * into params/error data); it only verifies the message "shape" so a UI can
 * surface a readable list of what is wrong. Returns every problem found, not
 * just the first.
 *
 * @param {*} obj - already-parsed message (the result of JSON.parse).
 * @returns {{ valid: boolean, errors: string[] }}
 */
export function validateRpc(obj) {
  const errors = [];
  if (typeof obj !== 'object' || obj === null) {
    return { valid: false, errors: ['Not an object.'] };
  }
  if (obj.jsonrpc !== JSONRPC_VERSION) errors.push('jsonrpc must be "2.0".');
  if ('method' in obj && typeof obj.method !== 'string') {
    errors.push('method must be a string.');
  }
  if ('result' in obj && 'error' in obj) {
    errors.push('cannot have both result and error.');
  }
  // A well-formed message is exactly one of: request/notification (method),
  // success response (result), or error response (error).
  if (!('method' in obj) && !('result' in obj) && !('error' in obj)) {
    errors.push('must have method, result, or error.');
  }
  return { valid: errors.length === 0, errors };
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →