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 →