JSON-RPC Request Builder — Go source
Build valid JSON-RPC 2.0 requests, notifications, success responses, and error responses, plus batch arrays. Validate message structure.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package jsonrpc is the Go twin of CosmoDev's src/lib/jsonRpc.ts (dual source:
// the web lib is TypeScript, the CLI lib is Go — kept in lock-step). Pure +
// deterministic, never panics. The table-driven tests in
// json-rpc-builder_test.go share vectors with src/lib/jsonRpc.test.ts so the
// two implementations are held to the same contract.
//
// The twin builds JSON-RPC 2.0 requests, notifications, success/error
// responses, and batches, mirroring the TS lib's public surface, defaults, and
// edge-case handling exactly. JSON output uses encoding/json; because struct
// fields are marshaled in declaration order, key ordering matches the TS
// objects (which follow JS insertion order).
package jsonrpc
import (
"encoding/json"
"reflect"
"strings"
)
// JSONRPCVersion is the JSON-RPC protocol version produced by every message,
// mirroring JSONRPC_VERSION in src/lib/jsonRpc.ts.
const JSONRPCVersion = "2.0"
// RpcID is a JSON-RPC id: a string, number, or null. It is an alias for any
// because Go has no union type; callers pass a string, a number (int/float),
// or nil for null — matching TS `RpcId = string | number | null`.
type RpcID = any
// Options mirrors BuildOptions in the TS lib. The zero value (Options{})
// matches the TS default: compact JSON (indent omitted → indent ?? 0 → 0).
type Options struct {
Indent int // <=0 → compact, matching TS indent ?? 0
}
// Outcome mirrors the TS Outcome: OK reports whether the build succeeded, JSON
// holds the serialized message ("" on failure), and Error holds a failure
// reason ("" when OK). It never represents a panic.
type Outcome struct {
OK bool
JSON string
Error string
}
// ValidationResult mirrors { valid, errors } from validateRpc in the TS lib.
type ValidationResult struct {
Valid bool
Errors []string
}
// StandardErrorCode carries the conventional message for a reserved code.
type StandardErrorCode struct {
Message string
}
// StandardErrors mirrors STANDARD_ERRORS in the TS lib: the conventional
// message for each reserved JSON-RPC error code.
var StandardErrors = map[int]StandardErrorCode{
-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"},
}
// Internal message shapes. Field declaration order is the JSON key order,
// matching the TS objects' JS insertion order. Optional fields (params, result,
// data) use omitempty so a nil value omits the key — mirroring TS, where the
// builder only sets the key when the value is not undefined.
type rpcRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params any `json:"params,omitempty"`
ID RpcID `json:"id"`
}
type rpcNotification struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params any `json:"params,omitempty"`
}
type rpcSuccess struct {
JSONRPC string `json:"jsonrpc"`
Result any `json:"result,omitempty"`
ID RpcID `json:"id"`
}
type rpcErrorBody struct {
Code int `json:"code"`
Message string `json:"message"`
Data any `json:"data,omitempty"`
}
type rpcError struct {
JSONRPC string `json:"jsonrpc"`
Error rpcErrorBody `json:"error"`
ID RpcID `json:"id"`
}
// safeStringify mirrors safeStringify() in the TS lib: compact JSON for
// indent <= 0, otherwise N-space pretty-printing identical to
// JSON.stringify(obj, null, indent). A marshal error is returned rather than
// panicking, so callers can surface it in an Outcome.
func safeStringify(v any, indent int) (string, error) {
var b []byte
var err error
if indent > 0 {
b, err = json.MarshalIndent(v, "", strings.Repeat(" ", indent))
} else {
b, err = json.Marshal(v)
}
if err != nil {
return "", err
}
return string(b), nil
}
// BuildRequest mirrors buildRequest() in the TS lib. method must be non-empty
// (the TS non-string guard is enforced by Go's type system; only the empty
// case remains). params may be nil to omit the field. id is always emitted —
// the TS default of 1 is supplied by the caller (or the cosmodev CLI layer).
func BuildRequest(method string, params any, id RpcID, opts Options) Outcome {
if method == "" {
return Outcome{OK: false, JSON: "", Error: "method must be a non-empty string"}
}
obj := rpcRequest{JSONRPC: JSONRPCVersion, Method: method, Params: params, ID: id}
s, err := safeStringify(obj, opts.Indent)
if err != nil {
return Outcome{OK: false, JSON: "", Error: err.Error()}
}
return Outcome{OK: true, JSON: s, Error: ""}
}
// BuildNotification mirrors buildNotification() in the TS lib: like a request
// but with no id (notifications are fire-and-forget).
func BuildNotification(method string, params any, opts Options) Outcome {
if method == "" {
return Outcome{OK: false, JSON: "", Error: "method must be a non-empty string"}
}
obj := rpcNotification{JSONRPC: JSONRPCVersion, Method: method, Params: params}
s, err := safeStringify(obj, opts.Indent)
if err != nil {
return Outcome{OK: false, JSON: "", Error: err.Error()}
}
return Outcome{OK: true, JSON: s, Error: ""}
}
// BuildSuccessResponse mirrors buildSuccessResponse() in the TS lib: a result
// response carrying the id of the originating request.
func BuildSuccessResponse(id RpcID, result any, opts Options) Outcome {
obj := rpcSuccess{JSONRPC: JSONRPCVersion, Result: result, ID: id}
s, err := safeStringify(obj, opts.Indent)
if err != nil {
return Outcome{OK: false, JSON: "", Error: err.Error()}
}
return Outcome{OK: true, JSON: s, Error: ""}
}
// BuildErrorResponse mirrors buildErrorResponse() in the TS lib. message and
// data are optional: an empty message falls back to the standard message for
// the code (then to "Error"), and a nil data value omits the data field.
func BuildErrorResponse(id RpcID, code int, message string, data any, opts Options) Outcome {
msg := message
if msg == "" {
if e, ok := StandardErrors[code]; ok {
msg = e.Message
} else {
msg = "Error"
}
}
obj := rpcError{
JSONRPC: JSONRPCVersion,
Error: rpcErrorBody{Code: code, Message: msg, Data: data},
ID: id,
}
s, err := safeStringify(obj, opts.Indent)
if err != nil {
return Outcome{OK: false, JSON: "", Error: err.Error()}
}
return Outcome{OK: true, JSON: s, Error: ""}
}
// BuildBatch mirrors buildBatch() in the TS lib: serializes a non-empty slice
// of pre-built messages as a JSON array. A non-array or empty array is rejected
// with the canonical error, matching TS Array.isArray + length checks.
func BuildBatch(messages any, opts Options) Outcome {
v := reflect.ValueOf(messages)
kind := v.Kind()
if kind != reflect.Slice && kind != reflect.Array {
return Outcome{OK: false, JSON: "", Error: "batch must be a non-empty array"}
}
if v.Len() == 0 {
return Outcome{OK: false, JSON: "", Error: "batch must be a non-empty array"}
}
s, err := safeStringify(messages, opts.Indent)
if err != nil {
return Outcome{OK: false, JSON: "", Error: err.Error()}
}
return Outcome{OK: true, JSON: s, Error: ""}
}
// ValidateRpc mirrors validateRpc() in the TS lib. It checks an in-memory
// object (a map[string]interface{} as produced by encoding/json) against the
// JSON-RPC 2.0 structural rules, collecting every violation. Anything that is
// not an object (nil, numbers, strings, booleans, arrays) is rejected with
// "Not an object." — matching the TS typeof guard.
func ValidateRpc(obj any) ValidationResult {
m, ok := obj.(map[string]any)
if !ok {
return ValidationResult{Valid: false, Errors: []string{"Not an object."}}
}
var errs []string
if m["jsonrpc"] != JSONRPCVersion {
errs = append(errs, `jsonrpc must be "2.0".`)
}
_, hasMethod := m["method"]
if hasMethod {
if _, isStr := m["method"].(string); !isStr {
errs = append(errs, "method must be a string.")
}
}
_, hasResult := m["result"]
_, hasError := m["error"]
if hasResult && hasError {
errs = append(errs, "cannot have both result and error.")
}
if !hasMethod && !hasResult && !hasError {
errs = append(errs, "must have method, result, or error.")
}
return ValidationResult{Valid: len(errs) == 0, Errors: errs}
}
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 →