Skip to content

JSON-RPC Request Builder — Zig source

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

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

// json-rpc-builder — Zig port: JSON-RPC 2.0 message builder + structural validation.
const std = @import("std");
const version = "2.0";

// Messages for the error codes the JSON-RPC 2.0 spec reserves (§5.1).
fn standardError(code: i32) []const u8 {
    return switch (code) {
        -32700 => "Parse error", -32600 => "Invalid Request", -32601 => "Method not found",
        -32602 => "Invalid params", -32603 => "Internal error", -32000 => "Server error",
        else => "Error",
    };
}
const Outcome = struct { ok: bool, json: []const u8 = "", err: ?[]const u8 = null };

// Params/id/data are pre-encoded JSON fragments; optionals splice in only when
// supplied — the analog of the TS lib's unset sentinel. Null id = notification.
fn buildMessage(a: std.mem.Allocator, method: []const u8, params: ?[]const u8, id: ?[]const u8) !Outcome {
    if (method.len == 0) return .{ .ok = false, .err = "method must be a non-empty string" };
    var buf = std.ArrayList(u8).init(a);
    const w = buf.writer();
    try w.print("{{\"jsonrpc\":\"{s}\",\"method\":\"{s}\"", .{ version, method });
    if (params) |p| try w.print(",\"params\":{s}", .{p});
    if (id) |i| try w.print(",\"id\":{s}", .{i});
    try w.writeAll("}");
    return .{ .ok = true, .json = buf.items };
}
fn buildErrorResponse(a: std.mem.Allocator, id: []const u8, code: i32, message: ?[]const u8, data: ?[]const u8) !Outcome {
    var buf = std.ArrayList(u8).init(a);
    const w = buf.writer();
    try w.print("{{\"jsonrpc\":\"{s}\",\"error\":{{\"code\":{d},\"message\":\"{s}\"", .{
        version, code, message orelse standardError(code),
    });
    if (data) |d| try w.print(",\"data\":{s}", .{d});
    try w.print("}},\"id\":{s}}}", .{id});
    return .{ .ok = true, .json = buf.items };
}

const Verdict = struct {
    valid: bool = true,
    errors: [3][]const u8 = undefined,
    n: usize = 0,
    fn add(self: *Verdict, msg: []const u8) void {
        self.errors[self.n] = msg; self.n += 1; self.valid = false;
    }
};

// Lightweight shape check on a parsed message — reports every problem found.
fn validateRpc(a: std.mem.Allocator, text: []const u8) !Verdict {
    var v = Verdict{};
    const parsed = std.json.parseFromSlice(std.json.Value, a, text, .{}) catch {
        v.add("Not an object.");
        return v;
    };
    defer parsed.deinit();
    if (std.meta.activeTag(parsed.value) != .object) {
        v.add("Not an object.");
        return v;
    }
    const obj = parsed.value.object;
    const jr = obj.get("jsonrpc");
    const okVersion = jr != null and std.meta.activeTag(jr.?) == .string and std.mem.eql(u8, jr.?.string, version);
    if (!okVersion) v.add("jsonrpc must be \"2.0\".");
    if (obj.get("method")) |m| if (std.meta.activeTag(m) != .string) v.add("method must be a string.");
    if (obj.get("result") != null and obj.get("error") != null)
        v.add("cannot have both result and error.");
    if (obj.get("method") == null and obj.get("result") == null and obj.get("error") == null)
        v.add("must have method, result, or error.");
    return v;
}
pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const a = arena.allocator();
    const w = std.io.getStdOut().writer();
    try w.print("{s}\n", .{(try buildMessage(a, "getBalance", "[\"0x1234\"]", "1")).json});
    try w.print("{s}\n", .{(try buildMessage(a, "blockHeader", null, null)).json});
    try w.print("{s}\n", .{(try buildErrorResponse(a, "1", -32601, null, null)).json});
    var verdict = try validateRpc(a, "{\"jsonrpc\":\"1.0\"}");
    for (verdict.errors[0..verdict.n]) |e| try w.print("error: {s}\n", .{e});
}

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 →