JSON in Blockchain: ABI Format, Transaction JSON, and NFT Metadata
Last updated:
JSON is the human-readable interface layer for blockchain — Ethereum smart contract ABIs, NFT metadata, transaction receipts, and RPC responses are all JSON objects. An Ethereum ABI JSON defines contract functions as an array of descriptors: [{"name":"transfer","type":"function","inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"outputs":[{"name":"","type":"bool"}],"stateMutability":"nonpayable"}]. NFT metadata follows the ERC-721 standard: {"name":"Token #1","description":"...","image":"ipfs://...","attributes":[{"trait_type":"Color","value":"Blue"}]}.
This guide covers Ethereum ABI JSON format, NFT metadata JSON (ERC-721/ERC-1155), JSON-RPC 2.0 for blockchain nodes, Solana transaction JSON, IPFS JSON storage, and reading on-chain JSON data with ethers.js. Whether you are building a dApp, indexing on-chain events, or minting an NFT collection, understanding these JSON formats is essential for working with any blockchain stack.
Key Terms
- ABI (Application Binary Interface)
- A JSON array that describes all public functions, events, errors, and the constructor of a smart contract. Used by dApp libraries (ethers.js, viem, web3.js) to encode calldata for transactions and decode return values and event logs. Without the ABI, on-chain bytecode is opaque.
- JSON-RPC 2.0
- A stateless, lightweight remote procedure call protocol that uses JSON as its data format. Ethereum nodes expose their API via JSON-RPC 2.0 over HTTP and WebSocket. Each request object has
jsonrpc,method,params, andidfields; the response mirrors theidand returns eitherresultorerror. - ERC-721
- The Ethereum standard for non-fungible tokens (NFTs). ERC-721 tokens have a
tokenURI(uint256 tokenId)function that returns a URI pointing to a JSON metadata file withname,description,image, and optionalattributes. Each token has a unique ID and is indivisible. - NFT Metadata
- The off-chain JSON file referenced by an NFT's token URI. It defines the human-readable name, description, image URL, and any traits displayed in marketplaces. Stored on IPFS, Arweave, or a centralized server. The metadata is not stored on-chain; only the URI hash is.
- IPFS CID
- A Content Identifier — the cryptographic hash of a file stored on the InterPlanetary File System. CIDv0 uses base58 encoding starting with
Qm; CIDv1 uses base32 encoding starting withbafybei. The same content always produces the same CID, making IPFS storage immutable and verifiable. - StateMutability
- An ABI field that classifies how a function interacts with the blockchain state.
"pure": no state read or write, no access to blockchain context."view": reads state but does not modify it (free to call)."nonpayable": modifies state, cannot receive ETH."payable": modifies state and can receive ETH. - Tuple type
- An ABI type representing a struct — a group of named values. In ABI JSON, tuples use
"type": "tuple"with a"components"array listing each field. Dynamic arrays of tuples use"type": "tuple[]". Ethers.js decodes tuples as JavaScript objects with named properties matching the component names.
Ethereum ABI JSON Format
The Ethereum ABI JSON is an array where each element describes one entry point of a smart contract. The type field is the primary discriminator: "function", "event", "error", or "constructor". Functions and events have inputs arrays; functions also have outputs arrays and a stateMutability field.
[
{
"name": "transfer",
"type": "function",
"stateMutability": "nonpayable",
"inputs": [
{ "name": "to", "type": "address" },
{ "name": "amount", "type": "uint256" }
],
"outputs": [{ "name": "", "type": "bool" }]
},
{
"name": "Transfer",
"type": "event",
"anonymous": false,
"inputs": [
{ "name": "from", "type": "address", "indexed": true },
{ "name": "to", "type": "address", "indexed": true },
{ "name": "value", "type": "uint256", "indexed": false }
]
},
{
"name": "balanceOf",
"type": "function",
"stateMutability": "view",
"inputs": [{ "name": "account", "type": "address" }],
"outputs": [{ "name": "", "type": "uint256" }]
}
]Tuple types (Solidity structs) use "type": "tuple" with a "components" array. Dynamic arrays of structs use "type": "tuple[]". The stateMutability field replaced the now-deprecated constant and payable boolean fields — use stateMutability in all new code. Custom errors (Solidity 0.8+) use "type": "error" with an inputs array describing the error parameters, enabling gas-efficient on-chain reverts with structured data.
NFT Metadata JSON (ERC-721 and ERC-1155)
ERC-721 NFT metadata JSON has three required fields and several optional ones widely supported by OpenSea and other marketplaces. The image field should be an IPFS URI for immutability; HTTP URLs are centralized and can disappear.
{
"name": "Cosmic Ape #42",
"description": "A rare cosmic ape from the Nebula collection.",
"image": "ipfs://bafybeig4jq7ybkxcl6pnqhfbezvfimh7wcqrjpwvhf5lfh7tnldmxpn3m/42.png",
"external_url": "https://cosmicapes.io/token/42",
"animation_url": "ipfs://bafybeig.../42.mp4",
"background_color": "1a1a2e",
"attributes": [
{ "trait_type": "Species", "value": "Nebula Ape" },
{ "trait_type": "Eyes", "value": "Laser" },
{ "trait_type": "Rarity", "value": "Legendary" },
{ "trait_type": "Power", "display_type": "boost_percentage", "value": 15 },
{ "trait_type": "Mint Date", "display_type": "date", "value": 1716163200 }
]
}ERC-1155 adds a decimals field (0 for NFTs, >0 for semi-fungible tokens) and typically uses a URI template with {id} as a placeholder that the contract replaces with the token ID. The animation_url field supports mp4, webm, mp3, wav, gltf, and glb — marketplaces render it as a media player. OpenSea additionally respects youtube_url for embedded video. Always validate metadata JSON against a schema before uploading to IPFS, as errors cannot be fixed once the CID is set on-chain.
JSON-RPC 2.0 for Blockchain Nodes
Every Ethereum node (Geth, Nethermind, Erigon) and RPC provider (Alchemy, Infura, QuickNode) speaks JSON-RPC 2.0. A request is a JSON object with exactly four fields; the id field is echoed back in the response to match async calls.
// Single request
{
"jsonrpc": "2.0",
"method": "eth_getBalance",
"params": ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "latest"],
"id": 1
}
// Response
{ "jsonrpc": "2.0", "result": "0x56bc75e2d63100000", "id": 1 }
// eth_call — read a view function without a transaction
{
"jsonrpc": "2.0",
"method": "eth_call",
"params": [
{
"to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"data": "0x70a08231000000000000000000000000d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
},
"latest"
],
"id": 2
}
// Batch request — array of request objects
[
{ "jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1 },
{ "jsonrpc": "2.0", "method": "eth_gasPrice", "params": [], "id": 2 }
]
// Error response
{
"jsonrpc": "2.0",
"error": { "code": -32602, "message": "Invalid params" },
"id": 1
}Standard error codes: -32700 parse error, -32600 invalid request, -32601 method not found, -32602 invalid params, -32603 internal error. Ethereum-specific codes start at -32000 (e.g., -32000 for execution reverted). Batch requests reduce HTTP round-trips but note that responses may return out of order — always match by id, never by array position.
Reading Contract Data with ethers.js
Ethers.js v6 abstracts JSON-RPC and ABI encoding/decoding into a clean TypeScript API. Pass the ABI JSON array (or human-readable ABI strings) to new ethers.Contract() and call functions directly as async methods.
import { ethers } from 'ethers'
const RPC_URL = 'https://mainnet.infura.io/v3/YOUR_KEY'
const provider = new ethers.JsonRpcProvider(RPC_URL)
// ERC-20 ABI (minimal — only the functions we need)
const erc20Abi = [
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function balanceOf(address) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
'event Transfer(address indexed from, address indexed to, uint256 value)',
]
const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
const contract = new ethers.Contract(USDC, erc20Abi, provider)
// Read state — free call, no gas
const [name, symbol, decimals] = await Promise.all([
contract.name(),
contract.symbol(),
contract.decimals(),
])
console.log(`${name} (${symbol}) — ${decimals} decimals`)
// USD Coin (USDC) — 6 decimals
const rawBalance = await contract.balanceOf('0xd8dA6BF...')
const formatted = ethers.formatUnits(rawBalance, decimals)
console.log('Balance:', formatted, symbol)
// Write — requires a signer (wallet)
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider)
const contractWithSigner = contract.connect(signer)
const tx = await contractWithSigner.transfer(
'0xRecipient...',
ethers.parseUnits('100', decimals) // 100 USDC in raw units
)
const receipt = await tx.wait()
console.log('Mined in block:', receipt.blockNumber)
// Listen for Transfer events
contract.on('Transfer', (from, to, value, event) => {
console.log(`${from} -> ${to}: ${ethers.formatUnits(value, decimals)} USDC`)
console.log('Tx hash:', event.log.transactionHash)
})For tuple (struct) return types, ethers.js decodes them as JavaScript objects with named properties matching the ABI component names — you can access result.amount directly instead of result[0]. TypeScript types can be generated from the ABI JSON using tools like typechain or wagmi generate, giving full autocomplete for contract function names and return types.
Solana Transaction JSON
Solana's getTransaction RPC returns a richer structure than Ethereum — it includes account keys, instruction data, and balance changes for both SOL and SPL tokens in a single response.
// getTransaction response (simplified)
{
"transaction": {
"message": {
"accountKeys": [
"7EcDhSYGxXyscszYEp35KHN8vvw3svAuLKTzXwCFLtV1",
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"11111111111111111111111111111111"
],
"header": {
"numRequiredSignatures": 1,
"numReadonlySignedAccounts": 0,
"numReadonlyUnsignedAccounts": 2
},
"instructions": [
{
"programIdIndex": 1,
"accounts": [0, 2],
"data": "3Bxs4h24hBtQy9rn"
}
],
"recentBlockhash": "9bs3FKCMVBe1Ky3pXoLmTVnkfLZ6tnHn4cXJSggjKT4"
},
"signatures": ["5sSf7..."]
},
"meta": {
"fee": 5000,
"preBalances": [1000000000, 0, 1],
"postBalances": [999995000, 0, 1],
"preTokenBalances": [],
"postTokenBalances": [],
"logMessages": [
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
],
"err": null
},
"slot": 280000000,
"blockTime": 1716163200
}Instructions reference accounts by index into accountKeys, keeping the transaction compact. The data field is base58-encoded (for legacy transactions) or base64-encoded (for versioned transactions) binary data — the program defines its own encoding format (e.g., Anchor uses Borsh). The logMessages array contains the program log output, useful for debugging failed transactions. Token balance changes appear in preTokenBalances and postTokenBalances arrays, each with accountIndex, mint, and uiTokenAmount.
IPFS JSON Storage for NFTs
IPFS (InterPlanetary File System) stores content by its cryptographic hash — the CID — rather than by location. The same JSON file always produces the same CID, making NFT metadata immutable once the CID is recorded on-chain.
import PinataSDK from '@pinata/sdk'
import { readFileSync } from 'fs'
const pinata = new PinataSDK({ pinataJWTKey: process.env.PINATA_JWT! })
// Upload a single NFT metadata JSON
async function uploadMetadata(tokenId: number) {
const metadata = {
name: `Cosmic Ape #${tokenId}`,
description: 'A rare cosmic ape from the Nebula collection.',
image: 'ipfs://bafybeig4jq7ybkxcl6pnqhfbezvfimh7wcqrjpwvhf5lfh7tnldmxpn3m/image.png',
attributes: [
{ trait_type: 'Species', value: 'Nebula Ape' },
{ trait_type: 'Rarity', value: 'Legendary' },
],
}
const result = await pinata.pinJSONToIPFS(metadata, {
pinataMetadata: { name: `cosmic-ape-${tokenId}.json` },
})
console.log('CID:', result.IpfsHash)
// bafybeig3q7... (CIDv0 from Pinata, starts with Qm or bafybei)
// Gateway URL for testing (not for production tokenURI)
const gatewayUrl = `https://gateway.pinata.cloud/ipfs/${result.IpfsHash}`
// Use ipfs:// URI for on-chain tokenURI
return `ipfs://${result.IpfsHash}`
}
// CIDv0 vs CIDv1
// CIDv0: Qm... (base58btc, SHA2-256, always 46 chars)
// CIDv1: bafybei... (multibase prefix b = base32)
// Convert CIDv0 to CIDv1 using the multiformats library:
import { CID } from 'multiformats/cid'
const cidV0 = CID.parse('QmXgqKTbzdh83pQtKFb19SpMCpDDcKR2ujqk3pKph9aqmz')
const cidV1 = cidV0.toV1()
console.log(cidV1.toString()) // bafybeig...Pin metadata with at least two services (e.g., Pinata + NFT.Storage) for redundancy. Arweave is an alternative to IPFS for permanent storage — it uses a one-time fee model and generates a transaction ID (ar://<txId>) as the URI. For large collections, upload images first to get their CIDs, embed those CIDs in the metadata JSON, then upload the metadata JSON. This ensures the metadata image field contains a final, stable IPFS URI before any metadata CIDs are recorded on-chain.
Validating Blockchain JSON
Invalid ABI JSON or malformed NFT metadata causes silent failures in dApps and incorrect rendering in marketplaces. Validate early — before deploying contracts or uploading to IPFS.
import Ajv from 'ajv'
const ajv = new Ajv({ allErrors: true })
// NFT metadata JSON Schema (ERC-721 / OpenSea compatible)
const nftMetadataSchema = {
type: 'object',
required: ['name', 'description', 'image'],
properties: {
name: { type: 'string', minLength: 1 },
description: { type: 'string' },
image: { type: 'string', pattern: '^(ipfs://|https://|ar://)' },
external_url: { type: 'string', format: 'uri' },
animation_url: { type: 'string' },
background_color: { type: 'string', pattern: '^[0-9a-fA-F]{6}$' },
attributes: {
type: 'array',
items: {
type: 'object',
required: ['value'],
properties: {
trait_type: { type: 'string' },
value: { oneOf: [{ type: 'string' }, { type: 'number' }] },
display_type: {
type: 'string',
enum: ['number', 'boost_percentage', 'boost_number', 'date'],
},
},
},
},
},
additionalProperties: true,
}
const validateNft = ajv.compile(nftMetadataSchema)
function validateMetadata(metadata: unknown) {
const valid = validateNft(metadata)
if (!valid) {
console.error('Validation errors:', validateNft.errors)
throw new Error('Invalid NFT metadata')
}
return true
}
// ABI validation — check required fields per entry type
function validateAbi(abi: unknown[]): boolean {
return abi.every(entry => {
if (typeof entry !== 'object' || entry === null) return false
const e = entry as Record<string, unknown>
if (!['function', 'event', 'error', 'constructor', 'fallback', 'receive'].includes(e.type as string)) return false
if (e.type === 'function' && !['pure', 'view', 'nonpayable', 'payable'].includes(e.stateMutability as string)) return false
return true
})
}For on-chain URI validation, call tokenURI(tokenId) using ethers.js, fetch the JSON from the returned URI (handling both ipfs:// and https:// schemes), and run it through the schema validator. This catches cases where the metadata was uploaded before the schema was finalized or where a centralized server returned HTML instead of JSON. See JSON data validation and JSON Schema patterns for advanced validation techniques.
FAQ
What is Ethereum ABI JSON format?
Ethereum ABI JSON is an array of objects describing every function, event, and error in a smart contract. Each object has a type field ("function", "event", "error", "constructor"), a name, inputs and outputs arrays, and a stateMutability field for functions. Libraries like ethers.js use the ABI to encode calldata (the hex bytes sent in a transaction) and decode return values. The ABI is generated by the Solidity compiler alongside the contract bytecode.
What JSON format do NFTs use for metadata?
NFTs use the ERC-721 metadata JSON standard: a JSON object with required fields name, description, and image (an IPFS or HTTPS URI). The optional attributes array holds trait objects, each with trait_type and value fields, plus an optional display_type ("boost_percentage", "date", "number") for special marketplace rendering. The metadata JSON is stored off-chain (usually IPFS) and referenced by the contract's tokenURI() function.
What is JSON-RPC 2.0 for blockchain?
JSON-RPC 2.0 is the protocol Ethereum nodes use to expose their API. A request is a JSON object with jsonrpc: "2.0", method (e.g. "eth_getBalance"), params (argument array), and id (matched in the response). The response contains either result (on success) or error with code and message. Batch requests send an array of request objects and receive an array of responses — match by id, not array position.
How do I read smart contract data with ethers.js?
Create a provider with new ethers.JsonRpcProvider(rpcUrl), then instantiate a contract: new ethers.Contract(address, abi, provider). Call view functions as async methods: const balance = await contract.balanceOf(address). For write operations, connect a Wallet signer to the contract: contract.connect(signer). Listen to events with contract.on("Transfer", handler). The ABI can be a JSON array or human-readable strings like "function balanceOf(address) view returns (uint256)".
What is the ERC-721 metadata JSON standard?
The ERC-721 metadata standard defines the off-chain JSON file referenced by tokenURI(). Required fields: name (token name), description (text description), image (media URI). Optional: attributes array with trait_type/value pairs, animation_url (video/audio), external_url (project site), background_color (6-char hex). OpenSea and other marketplaces read this format to display token traits, images, and special UI elements like percentage bars for display_type: "boost_percentage" attributes.
How do I store NFT metadata JSON on IPFS?
Upload the metadata JSON to a pinning service like Pinata, NFT.Storage, or web3.storage using their SDK or API. After upload, you receive a CID. Set your contract's base URI or per-token URI to ipfs://<CID>. For a collection, upload all images first to get their CIDs, embed them in metadata JSON files, then upload the metadata to get their CIDs. Prefer CIDv1 (bafybei...) over CIDv0 (Qm...) for new projects. Pin with at least two services for redundancy.
What is the Solana transaction JSON format?
Solana's getTransaction response contains a transaction object with message.accountKeys (array of base58 public keys), message.instructions (each with programIdIndex, accounts as index arrays, and base58/base64 data), and signatures. The meta object holds fee, preBalances/postBalances for SOL changes, preTokenBalances/postTokenBalances for SPL token changes, and logMessages for program output. Instructions reference accounts by index into accountKeys — not by public key directly.
How do I validate NFT metadata JSON?
Use a JSON Schema validator like Ajv with a schema requiring name, description, and image as strings, and validating the attributes array structure. Check that image starts with ipfs://, https://, or ar://. Validate that display_type values (if present) are in the allowed set. For on-chain validation, call tokenURI() with ethers.js, fetch the JSON, and run it through the schema. Run validation before uploading to IPFS — once the CID is set on-chain, the metadata URL is immutable.
Further reading and primary sources
- Ethereum ABI Specification — Official Solidity documentation on ABI encoding and JSON format
- ERC-721 Metadata Standard (OpenSea) — OpenSea metadata standards for ERC-721 and ERC-1155 NFTs
- JSON-RPC 2.0 Specification — Official JSON-RPC 2.0 protocol specification
- ethers.js v6 Documentation — Complete ethers.js v6 API reference for contract interaction
- IPFS Content Addressing — How IPFS CIDs work and the difference between CIDv0 and CIDv1