JavaScript Base64 recipes for JSON, files and byte arrays

Use UTF-8-safe JavaScript recipes for JSON payloads and selected files without accidentally treating binary data as text.

Useful Base64 code starts by deciding what the input actually is. A JavaScript string needs a character encoding; a file already has bytes; a JSON value needs serialization first. Keeping those steps visible makes debugging easier and prevents accidental double encoding.

Encode a small JSON value

Serialize the value exactly once, encode that string as UTF-8, then encode the bytes as Base64. This example includes a non-ASCII field to catch implementations that work only with English. It is intended for small payloads; the temporary byte string increases memory usage.

const value = { message: '你好', count: 3 };
const text = JSON.stringify(value);
let binary = '';
for (const byte of new TextEncoder().encode(text)) {
  binary += String.fromCharCode(byte);
}
const encoded = btoa(binary);
const bytes = Uint8Array.from(atob(encoded), c => c.charCodeAt(0));
const json = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
console.log(JSON.parse(json).message); // 你好

A successful Base64 decode does not guarantee valid JSON. Treat the UTF-8 decode and JSON.parse as separate failure points. When the decoded content is already a string containing JSON, serializing it again adds quotation marks and escapes; that changes the payload.

Read a selected file as bytes

For a browser File, use arrayBuffer() when you need the underlying bytes. Do not use file.text() for an image, archive or encrypted file: invalid character sequences can become replacement characters. If your destination needs a Data URI, FileReader.readAsDataURL() provides the wrapper as well as the Base64 data.

// file is a File selected by the user.
const bytes = new Uint8Array(await file.arrayBuffer());
console.log(bytes.byteLength); // original file size in bytes
// Pass bytes to a byte-oriented encoder, not TextEncoder.

Roundtrip checks that catch real mistakes

Compare decoded bytes with the original byte array. Comparing screenshots or file names cannot detect a changed byte. Include empty input, all byte values from 0 to 255, Chinese text, emoji and a payload whose length leaves one or two bytes in the final group. For a batch of files, preserve each file's name, size and result independently so one error cannot overwrite another successful output.

Keep conversions local unless your workflow intentionally calls an API. A network request containing the encoded output still sends the underlying data in reversible form. For the byte-string behavior and Unicode limitation of the browser function, see MDN: btoa().