Skip to content

JSON-RPC Request Builder — Kotlin source

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

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

// json-rpc-builder — Kotlin port: JSON-RPC 2.0 message builder + structural validation.

data class Outcome(val ok: Boolean, val json: String, val error: String? = null)

/** Sentinel telling "argument absent" apart from a legitimate JSON null. */
private val UNSET = Any()

fun standardError(code: Int): String = when (code) {
    -32700 -> "Parse error"; -32600 -> "Invalid Request"; -32601 -> "Method not found"
    -32602 -> "Invalid params"; -32603 -> "Internal error"; -32000 -> "Server error"
    else -> "Error"
}

/** Minimal JSON writer — insertion-ordered Map, List, String, Number, Boolean, null. */
fun toJson(v: Any?): String = when (v) {
    null -> "null"
    is String -> "\"" + v.replace("\\", "\\\\").replace("\"", "\\\"") + "\""
    is Number, is Boolean -> v.toString()
    is List<*> -> v.joinToString(",", "[", "]") { toJson(it) }
    is Map<*, *> -> v.entries.joinToString(",", "{", "}") { "${toJson(it.key)}:${toJson(it.value)}" }
    else -> throw IllegalArgumentException("unsupported: ${v::class}")
}

private fun base(method: String) = linkedMapOf<String, Any?>("jsonrpc" to "2.0", "method" to method)

fun buildRequest(method: String?, params: Any? = UNSET, id: Any? = 1): Outcome {
    if (method.isNullOrEmpty()) return Outcome(false, "", "method must be a non-empty string")
    val obj = base(method)
    if (params !== UNSET) obj["params"] = params
    obj["id"] = id
    return Outcome(true, toJson(obj))
}

// A notification is a request without an id: fire-and-forget, no reply.
fun buildNotification(method: String?, params: Any? = UNSET): Outcome {
    if (method.isNullOrEmpty()) return Outcome(false, "", "method must be a non-empty string")
    val obj = base(method)
    if (params !== UNSET) obj["params"] = params
    return Outcome(true, toJson(obj))
}

fun buildSuccessResponse(id: Any?, result: Any?): Outcome {
    val obj = linkedMapOf<String, Any?>("jsonrpc" to "2.0", "result" to result, "id" to id)
    return Outcome(true, toJson(obj))
}

fun buildErrorResponse(id: Any?, code: Int, message: String? = null, data: Any? = UNSET): Outcome {
    val error = linkedMapOf<String, Any?>("code" to code, "message" to (message ?: standardError(code)))
    if (data !== UNSET) error["data"] = data
    val obj = linkedMapOf<String, Any?>("jsonrpc" to "2.0", "error" to error, "id" to id)
    return Outcome(true, toJson(obj))
}

// Per spec §6 a batch is a non-empty array sent in one round-trip.
fun buildBatch(messages: Any?): Outcome {
    if (messages !is List<*> || messages.isEmpty())
        return Outcome(false, "", "batch must be a non-empty array")
    return Outcome(true, toJson(messages))
}

/** Lightweight shape check — reports every problem, not just the first. */
fun validateRpc(obj: Any?): Pair<Boolean, List<String>> {
    if (obj !is Map<*, *>) return false to listOf("Not an object.")
    val errors = buildList {
        if (obj["jsonrpc"] != "2.0") add("jsonrpc must be \"2.0\".")
        if ("method" in obj && obj["method"] !is String) add("method must be a string.")
        if ("result" in obj && "error" in obj) add("cannot have both result and error.")
        if (listOf("method", "result", "error").none { it in obj })
            add("must have method, result, or error.")
    }
    return (errors.isEmpty()) to errors
}

fun main() {
    println(buildRequest("getBalance", listOf("0x1234"), 1).json)
    println(buildNotification("blockHeader").json)
    println(buildSuccessResponse(1, "0x1bc16d674ec80000").json)
    println(buildErrorResponse(1, -32601).json)
    println(validateRpc(mapOf("jsonrpc" to "1.0")))
}

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 →