jsonweb3blockchainethereum

Web3 JSON: Ethereum JSON-RPC, ABIs & Transaction Receipts

·10 min read·Advanced

Blockchains speak JSON at every layer

Whether you're calling a node, reading a smart contract's interface, or checking whether a transaction succeeded, you're working with JSON. Ethereum and most EVM-compatible chains (Polygon, Base, Arbitrum) expose their entire node API as JSON-RPC, describe every smart contract's callable functions as a JSON ABI, and return every transaction's outcome as a JSON receipt. None of it is blockchain-specific magic — it's the same JSON-RPC 2.0 shape used by MCP and other tool protocols, applied to a different domain.

Talking to a node: JSON-RPC

Every Ethereum node — whether run by Infura, Alchemy, or your own — accepts JSON-RPC 2.0 requests over HTTP:

json
// Request — get the latest block number
{ "jsonrpc": "2.0", "id": 1, "method": "eth_blockNumber", "params": [] }
json
// Response — a hex-encoded number, per Ethereum's own convention
{ "jsonrpc": "2.0", "id": 1, "result": "0x1234567" }

Note the result is a hex string, not a JSON number — Ethereum's JSON-RPC spec encodes all quantities this way to avoid ambiguity around leading zeros and to support values far beyond JavaScript's safe integer range. Client libraries (ethers.js, web3.js, viem) convert this for you automatically.

json
// Request — read an account's balance
{
  "jsonrpc": "2.0", "id": 2, "method": "eth_getBalance",
  "params": ["0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", "latest"]
}

The ABI: a contract's JSON interface

A smart contract's Application Binary Interface (ABI) is a JSON array describing every function, event, and error the contract exposes — conceptually the same role an OpenAPI spec plays for a REST API, but for a contract's on-chain functions:

json
[
  {
    "type": "function",
    "name": "transfer",
    "inputs": [
      { "name": "to", "type": "address" },
      { "name": "amount", "type": "uint256" }
    ],
    "outputs": [{ "name": "success", "type": "bool" }],
    "stateMutability": "nonpayable"
  },
  {
    "type": "event",
    "name": "Transfer",
    "inputs": [
      { "name": "from", "type": "address", "indexed": true },
      { "name": "to", "type": "address", "indexed": true },
      { "name": "value", "type": "uint256", "indexed": false }
    ]
  }
]

Client libraries read this JSON to know how to encode a function call into the raw bytes a contract expects, and how to decode the raw bytes a contract returns (or the raw event logs it emits) back into typed values:

javascript
// ethers.js — the ABI JSON drives both encoding and decoding
import { Contract } from "ethers";

const contract = new Contract(contractAddress, abiJson, signer);
await contract.transfer("0x742d...", 1000000n); // encodes using the ABI

Transaction receipts: what actually happened

Submitting a transaction doesn't tell you whether it succeeded — for that, you fetch its receipt once mined:

json
{
  "transactionHash": "0xabc123...",
  "blockNumber": "0x1234568",
  "status": "0x1",
  "gasUsed": "0x5208",
  "from": "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0",
  "to": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
  "logs": [
    {
      "address": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984",
      "topics": [
        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
        "0x000000000000000000000000742d35cc6634c0532925a3b844bc9e7595f0be"
      ],
      "data": "0x00000000000000000000000000000000000000000000000000000000000f4240"
    }
  ]
}

`status: "0x1"` means success, `"0x0"` means the transaction reverted — this is the single field most application code checks first. logs contains the raw event logs the transaction emitted; a client library uses the contract's ABI to decode topics/data back into the named, typed event fields (like from, to, value for a Transfer event) shown in the ABI example above.

Why hex strings everywhere?

Ethereum values (balances, gas, block numbers) can exceed Number.MAX_SAFE_INTEGER by many orders of magnitude — a token can have 18 decimal places and a total supply in the billions, producing numbers no JSON number can represent without precision loss in JavaScript. Encoding every quantity as a hex string sidesteps that entirely; libraries parse it into a big-integer type (BigInt in modern JavaScript, or a dedicated big-number class) rather than a native number.

Debugging Web3 JSON in practice

  1. Format raw RPC responses. Node responses are minified by default — paste one into JSONKit's JSON Formatter to read a deeply nested receipt or block object.
  2. Validate an ABI's shape. A malformed ABI JSON causes cryptic encoding errors — check it parses cleanly with the JSON Validator before debugging further upstream.
  3. Generate types from a contract's ABI. Paste an ABI into JSON to TypeScript to get a rough type shape for a contract's functions and events as a starting point (dedicated ABI-to-types tools like TypeChain go further, but this is a fast sanity check).

Frequently asked questions

Because on-chain values (token amounts, gas, block numbers) routinely exceed what a JSON/JavaScript number can represent exactly. Hex strings avoid precision loss; client libraries convert them to a big-integer type.

It's the JSON description of a smart contract's callable functions and emitted events, which a client library uses to encode calls into raw bytes and decode raw responses/logs back into typed, named values.

Check the transaction receipt's status field: "0x1" means it succeeded, "0x0" means it reverted. The receipt is only available once the transaction is mined.

No — it's a general-purpose protocol (also used by MCP and many developer tools) for request/response and notification messages; Ethereum is simply one well-known adopter, with its own domain-specific methods layered on top of the standard JSON-RPC 2.0 envelope.

Try JSONKit — JSON Developer Tools

Format, validate, convert and diff JSON. Instant, accurate and 100% private.