Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Integration & Development14 min read

Solana jsonParsed vs base64: Transaction Encoding

Choose between jsonParsed and base64 for Solana transaction reads by understanding parser guarantees, versioned message resolution, and client-side decoding tradeoffs.

TL;DR

Solana JSON-RPC transaction reads accept an encoding parameter that controls whether the node returns raw serialized bytes (base64 or base64+zstd) or a best-effort structured interpretation (jsonParsed). jsonParsed decodes only instructions whose program the node recognises; unknown programs appear as partiallyDecoded with raw data, so consumers that assume full decoding will fail on first contact with an unfamiliar program. base64 returns deterministic bytes that do not depend on node parser coverage or version, making it the stable choice for indexers and cross-provider reproducibility, at the cost of maintaining a client-side Borsh or bincode decoder. Versioned transactions add a wrinkle: resolving instruction accounts requires merging static message keys with addresses loaded from lookup tables in the documented order. This article explains the encoding vocabulary, the guarantees each encoding provides, the interaction with maxSupportedTransactionVersion, and provides runnable Node.js examples that compare both encodings and reconstruct instructions from base64 alone.

Encoding vocabulary for Solana transaction reads

Solana JSON-RPC methods that return transactions—getTransaction, getBlock, and the transactionSubscribe family—accept an encoding parameter that determines the representation of the transaction payload. The documented values are base64 (raw serialized transaction bytes), base64+zstd (the same bytes compressed with zstd), and jsonParsed (a best-effort structured interpretation of the message, its instructions, and the loaded addresses). The request and response envelope follows the JSON-RPC 2.0 specification, so the encoding choice affects only the result payload, not the framing.

The encoding parameter is not a formatting preference; it selects between two fundamentally different contracts. base64 and base64+zstd return the canonical serialized transaction. jsonParsed returns a node-generated interpretation that depends on the node's parser coverage and software version. The Solana documentation for getTransaction enumerates these values and the response shape, and the RPC JSON structures page documents the jsonParsed and partiallyDecoded instruction forms.

For teams building on managed endpoints, the encoding decision is independent of the provider. The Solana network page describes the RPC surface, and the Solana RPC methods reference lists the methods that accept encoding. The choice is yours; the node merely honours it.

  • base64: raw serialized transaction bytes, deterministic across nodes and versions.
  • base64+zstd: the same bytes compressed; requires a zstd decoder before base64 decoding.
  • jsonParsed: structured message and instructions, best-effort, node-version sensitive.

What jsonParsed actually guarantees

jsonParsed does not guarantee that every instruction is decoded. The runtime parses instructions whose program it understands into a structured form and leaves the rest as partiallyDecoded, carrying the raw instruction data and account indices. A consumer that assumes every instruction has a parsed field will crash on the first unknown program. This is the single most common source of 'jsonParsed is inconsistent' bug reports: the same transaction can appear fully parsed on one node and partially decoded on another if parser coverage differs.

The partiallyDecoded form is not an error. It is the documented representation for instructions the node cannot interpret. Your code must branch on the presence of parsed versus partiallyDecoded and fall back to raw data when needed. The RPC JSON structures page shows both forms side by side.

Because jsonParsed is best-effort, it is unsuitable as the sole source for indexers that must diff records field-by-field across providers or over time. The structured fields can change when the node's parser is updated, even if the underlying transaction bytes are identical.

  • parsed: instruction data and accounts are structured by the node.
  • partiallyDecoded: raw data and account indices are returned; the client must decode.
  • Node version and parser coverage determine which form you receive.

Why base64 is the stable choice for indexers

base64 returns the serialized transaction bytes exactly as they were included in the ledger. The bytes never depend on the node's parser coverage or software version, so the client owns a Borsh or bincode layout and the data is diffable and reproducible across providers and over time. The cost is maintaining the decoder: you must track program layouts and account structures yourself.

For indexers, the determinism of base64 outweighs the convenience of jsonParsed. A record built from base64 can be compared byte-for-byte with a record from another provider. A record built from jsonParsed cannot, because the structured fields are a node-generated interpretation. If you need both convenience and determinism, fetch base64 and decode locally, or fetch both encodings and reconcile.

The versioned transactions and getBlock parsing article covers how getBlock returns versioned transactions and why the raw bytes are the canonical source. The getTransaction meta and inner instructions article covers the post-send meta object, which is separate from the encoding choice.

  • Deterministic across nodes, providers, and time.
  • Requires a client-side Borsh or bincode decoder.
  • Enables byte-level diffing and reproducible indexing.

Versioned transaction resolution and account lookup tables

A v0 message's static keys and the addresses resolved from address lookup tables must both be present before an instruction's accounts can be resolved. A decoder must merge the message's account keys with the loaded addresses in the documented order rather than indexing the static key list alone. If you index only the static keys, instruction accounts that reference loaded addresses will resolve to the wrong pubkeys or out of range.

jsonParsed does not remove this requirement; the node performs the merge for you when it can, but the underlying resolution rules are the same. When you decode base64 yourself, you must implement the merge. The versioned transactions and getBlock parsing article details the order and the lookup table mechanics.

The encoding interacts with getTransaction's maxSupportedTransactionVersion parameter. A version-gated response can be an error rather than a decoded message when the requested version is unsupported, and jsonParsed does not remove that gate. You must set maxSupportedTransactionVersion to the highest version you can handle, or the node may reject the request.

  • Merge static account keys with loaded addresses in documented order.
  • jsonParsed performs the merge when it can, but the rules are unchanged.
  • maxSupportedTransactionVersion gates the response regardless of encoding.

Runnable comparison: base64 vs jsonParsed for the same signature

The following Node.js script fetches the same transaction signature twice—once with base64 and once with jsonParsed—and reports where the two disagree. It highlights partiallyDecoded instructions and missing inner instructions. Run it against your own endpoint by setting the RPC_URL environment variable. The script uses the built-in fetch API available in Node.js 18+.

The script does not assert that one encoding is better; it surfaces the differences so you can decide. The output is a report you can extend with your own program layouts.

const RPC_URL = process.env.RPC_URL || 'https://your-endpoint.example';
const SIGNATURE = process.env.SIGNATURE;

async function rpc(method, params) {
  const res = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  const json = await res.json();
  if (json.error) throw new Error(JSON.stringify(json.error));
  return json.result;
}

async function fetchBoth(signature) {
  const base64 = await rpc('getTransaction', [
    signature,
    { encoding: 'base64', maxSupportedTransactionVersion: 0 }
  ]);
  const jsonParsed = await rpc('getTransaction', [
    signature,
    { encoding: 'jsonParsed', maxSupportedTransactionVersion: 0 }
  ]);
  return { base64, jsonParsed };
}

function reportDifferences(base64, jsonParsed) {
  const b64Ix = base64.transaction.message.instructions;
  const jpIx = jsonParsed.transaction.message.instructions;
  console.log('base64 instruction count:', b64Ix.length);
  console.log('jsonParsed instruction count:', jpIx.length);
  jpIx.forEach((ix, i) => {
    if (ix.parsed === undefined) {
      console.log(`jsonParsed instruction ${i} is partiallyDecoded`);
    }
  });
  const b64Inner = base64.meta?.innerInstructions || [];
  const jpInner = jsonParsed.meta?.innerInstructions || [];
  console.log('base64 inner instruction groups:', b64Inner.length);
  console.log('jsonParsed inner instruction groups:', jpInner.length);
}

(async () => {
  const { base64, jsonParsed } = await fetchBoth(SIGNATURE);
  reportDifferences(base64, jsonParsed);
})();

Reconstructing the instruction list from base64 alone

To reconstruct instructions from base64, you must deserialize the transaction, resolve account keys (including loaded addresses for v0), and decode each instruction's data against its program layout. The following Node.js example uses @solana/web3.js to deserialize a base64 transaction and print the instruction list with program IDs and account keys. It does not decode instruction data; that requires a program-specific layout.

This approach gives you a deterministic instruction list that does not depend on the node's parser. You can extend it by adding Borsh or bincode decoders for the programs you care about. The getTransaction meta and inner instructions article covers how to pair this with the meta object for inner instructions.

const { Connection, PublicKey, VersionedTransaction } = require('@solana/web3.js');

const RPC_URL = process.env.RPC_URL || 'https://your-endpoint.example';
const SIGNATURE = process.env.SIGNATURE;

(async () => {
  const connection = new Connection(RPC_URL, 'confirmed');
  const tx = await connection.getTransaction(SIGNATURE, {
    encoding: 'base64',
    maxSupportedTransactionVersion: 0
  });
  if (!tx) throw new Error('Transaction not found');
  const raw = Buffer.from(tx.transaction[0], 'base64');
  const decoded = VersionedTransaction.deserialize(raw);
  const message = decoded.message;
  const staticKeys = message.staticAccountKeys.map(k => k.toBase58());
  const loaded = tx.meta?.loadedAddresses || { writable: [], readonly: [] };
  const allKeys = [
    ...staticKeys,
    ...loaded.writable,
    ...loaded.readonly
  ];
  message.compiledInstructions.forEach((ix, i) => {
    const programId = allKeys[ix.programIdIndex];
    const accounts = ix.accountKeyIndexes.map(idx => allKeys[idx]);
    console.log(`Instruction ${i}: program=${programId}`);
    console.log('  accounts:', accounts.join(', '));
    console.log('  data length:', ix.data.length);
  });
})();

Decision table: encoding vs determinism vs client work vs node-version sensitivity

The following table summarises the tradeoffs. Verify each row against your own endpoint by fetching the same signature with each encoding and recording the results. The table is a guide, not a benchmark; your node's parser coverage and version determine the actual behaviour.

Use the table to choose an encoding per use case. For indexers, base64 is the stable choice. For exploratory tooling, jsonParsed reduces client work. For bandwidth-constrained environments, base64+zstd reduces payload size at the cost of a decompression step.

  • base64: determinism high; client work high (full decoder); node-version sensitivity low.
  • base64+zstd: determinism high; client work high plus zstd; node-version sensitivity low.
  • jsonParsed: determinism low; client work low for known programs; node-version sensitivity high.
  • jsonParsed with partiallyDecoded fallback: determinism medium; client work medium; node-version sensitivity medium.

Measuring encoding behaviour against your own endpoint

Because parser coverage and node versions vary, you should measure the behaviour of your endpoint rather than rely on general claims. The method below is reproducible: fetch a set of signatures with both encodings, count partiallyDecoded instructions, and record the differences. Fill the results table with your own numbers.

Run the comparison script from the earlier section over a sample of signatures that includes known programs (System, Token, Associated Token) and at least one unfamiliar program. Record the counts. Repeat the measurement after any node upgrade to detect changes in parser coverage.

  • Results Table columns: signature, base64 instruction count, jsonParsed instruction count, partiallyDecoded count, inner instruction group count (base64), inner instruction group count (jsonParsed).
  • Row 1: [fill with your measurement]
  • Row 2: [fill with your measurement]
  • Row 3: [fill with your measurement]

Limitations and tradeoffs

jsonParsed is best-effort and node-version sensitive. It is convenient for known programs but unsuitable as the sole source for deterministic indexing. base64 is deterministic but requires a client-side decoder that you must maintain as program layouts evolve. base64+zstd adds a decompression dependency.

Versioned transactions require merging static keys with loaded addresses; failing to do so produces incorrect account resolution. The encoding choice does not remove this requirement. The Solana network page and RPC pricing pages describe the endpoint surface and cost model; the API service page describes managed access.

No encoding eliminates the need to understand the transaction format. Choose based on whether you value determinism (base64) or reduced client work (jsonParsed), and measure against your own endpoint.

  • jsonParsed: low client work, low determinism.
  • base64: high client work, high determinism.
  • Versioned transactions: merge keys regardless of encoding.

Next steps for integration

Start by fetching a known signature with both encodings and running the comparison script. Record the differences in the results table. Then decide which encoding fits your use case. For indexers, build a base64 decoder for the programs you care about. For tooling, use jsonParsed with a partiallyDecoded fallback.

Review the OnFinality Learn hub for related articles on Solana RPC integration, and the Solana RPC methods reference for the full method list. The accountSubscribe encoding article covers subscription encodings, and the versioned transactions article covers getBlock parsing.

Measure, decide, and document your encoding choice in your integration notes so future maintainers understand why the client decodes base64 or relies on jsonParsed.

  • Run the comparison script on your endpoint.
  • Choose base64 for determinism, jsonParsed for convenience.
  • Document the choice and the fallback behaviour.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started