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 →