Skip to main content

πŸ“„ Working with Files and JSON

Reading a config file, writing a log, caching an API response to disk β€” backends do this constantly, and JSON is the lingua franca they use to do it. This lesson lines up file I/O and JSON handling in Node.js, Python, and PHP so you can see that the steps are identical: open, transfer bytes, decode or encode, and handle the things that can go wrong.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Read and write text files in Node.js, Python, and PHP
  • Parse JSON into native data and serialize native data back to JSON in each language
  • Explain text vs binary mode and why UTF-8 encoding matters
  • Handle the common error cases β€” missing files, permission denied, malformed JSON
  • Combine the two into a real task: load, modify, and save a JSON data file safely

Estimated Time: 30–40 minutes  β€’  Difficulty: Intermediate

Hands-on: Build a tiny "visit counter" that reads a JSON file, increments a number, and writes it back β€” in all three languages.

In This Lesson

The Shape of File I/O

A file on disk is just a sequence of bytes. To turn those bytes into something useful your program does two steps in a row: it reads the raw bytes into a string, then it decodes that string into a structure (often via JSON). Writing runs the same pipeline in reverse.

The file plus JSON pipeline Bytes on disk are read into a text string, then parsed into a native object; writing reverses the flow by serializing an object to text and writing bytes. File on disk raw bytes Text string decoded UTF-8 Native object dict / obj / array read parse stringify write
Figure 1 β€” Reading goes left-to-right (bytes β†’ text β†’ object); writing goes right-to-left (object β†’ text β†’ bytes). Every language implements these same two hops.

πŸ“– Key Terms

Serialize (stringify / encode): turn an in-memory object into a text form that can be stored or sent.

Deserialize (parse / decode): turn stored text back into an in-memory object.

Encoding: the rule (almost always UTF-8) mapping characters to bytes.

Reading a Text File

The simplest case: read an entire text file into a string. Note that Node's file API is asynchronous by default β€” you await it β€” while Python and PHP read synchronously.

// Node.js β€” the modern promise-based API
import { readFile } from 'node:fs/promises';

const text = await readFile('notes.txt', 'utf8');
console.log(text);
# Python β€” open() with a context manager (auto-closes)
with open('notes.txt', 'r', encoding='utf-8') as f:
    text = f.read()
print(text)
<?php
// PHP β€” one call reads the whole file into a string
$text = file_get_contents('notes.txt');
echo $text;

⚠️ Always specify the encoding

If you omit 'utf8' in Node's readFile, you get a raw Buffer of bytes, not a string. In Python, omitting encoding='utf-8' falls back to the platform default, which can mangle non-ASCII text on some systems. Be explicit β€” UTF-8 is the safe, portable choice.

Reading line by line

For large files you often don't want the whole thing in memory at once:

# Python β€” iterate a file object line by line, lazily
with open('big.log', 'r', encoding='utf-8') as f:
    for line in f:
        process(line.rstrip('\n'))
<?php
// PHP β€” file() returns an array of lines
foreach (file('big.log', FILE_IGNORE_NEW_LINES) as $line) {
    process($line);
}

Writing a Text File

Writing replaces the file's contents (or creates it if absent). A separate append mode adds to the end instead.

// Node.js
import { writeFile, appendFile } from 'node:fs/promises';

await writeFile('out.txt', 'Hello\n', 'utf8');   // overwrite / create
await appendFile('out.txt', 'Another line\n');    // add to the end
# Python β€” mode 'w' overwrites, 'a' appends
with open('out.txt', 'w', encoding='utf-8') as f:
    f.write('Hello\n')

with open('out.txt', 'a', encoding='utf-8') as f:
    f.write('Another line\n')
<?php
// PHP β€” file_put_contents overwrites; add FILE_APPEND to append
file_put_contents('out.txt', "Hello\n");
file_put_contents('out.txt', "Another line\n", FILE_APPEND);
TaskNode.jsPythonPHP
Read whole filereadFile(p, 'utf8')open(p).read()file_get_contents(p)
Write (overwrite)writeFile(p, s)open(p, 'w')file_put_contents(p, s)
AppendappendFile(p, s)open(p, 'a')…, FILE_APPEND
Sync or async?Async (await)SyncSync

πŸ’‘ Node also has a sync flavour

Node offers readFileSync / writeFileSync for scripts and startup code where blocking is fine. Inside a request handler, though, prefer the fs/promises versions so one slow disk read doesn't freeze the whole server.

Parsing & Serializing JSON

JSON is just a string in a specific format. Parsing turns that string into a native structure; serializing turns a native structure back into a string. This step is entirely separate from touching the disk.

Parse: text β†’ object

// Node.js
const obj = JSON.parse('{"name":"Ada","tags":["a","b"]}');
obj.name;      // 'Ada'  -> a normal JS object
# Python
import json
obj = json.loads('{"name":"Ada","tags":["a","b"]}')
obj['name']    # 'Ada'  -> a dict
<?php
// PHP β€” pass true to get an associative array (else you get stdClass)
$obj = json_decode('{"name":"Ada","tags":["a","b"]}', true);
$obj['name'];  // 'Ada'

Serialize: object β†’ text

// Node.js β€” the 3rd arg is indent width for pretty output
const data = { name: 'Ada', tags: ['a', 'b'] };
JSON.stringify(data);          // '{"name":"Ada","tags":["a","b"]}'
JSON.stringify(data, null, 2); // pretty, 2-space indent
# Python β€” indent= for pretty; ensure_ascii=False keeps Γ©, δΈ­ as-is
json.dumps(data)                          # compact
json.dumps(data, indent=2, ensure_ascii=False)  # pretty + unicode
<?php
// PHP β€” flags control formatting
json_encode($data);                                    // compact
json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);

⚠️ JSON is stricter than it looks

Real JSON requires double quotes on keys and strings, forbids trailing commas, and has no comments. A single quote or a stray comma makes JSON.parse throw a SyntaxError, json.loads raise JSONDecodeError, and json_decode return null. Never trust JSON from a user or network without wrapping the parse in error handling.

πŸ“– How types map

JSON object β†’ JS object / Python dict / PHP array. JSON array β†’ all three's list type. JSON null β†’ null / None / null. Note JSON has no date type β€” dates travel as strings and you re-parse them yourself.

The Read–Modify–Write Round Trip

Combine the two halves and you get the everyday pattern: load a JSON file into an object, change something, and write it back. Here it is end-to-end in each language, with the parse guarded against a malformed or missing file.

flowchart LR A["Read file (text)"] --> B["Parse JSON to object"] B --> C["Modify the object"] C --> D["Stringify to JSON text"] D --> E["Write file"]
// Node.js β€” bump a counter stored in data.json
import { readFile, writeFile } from 'node:fs/promises';

let data;
try {
  data = JSON.parse(await readFile('data.json', 'utf8'));
} catch (err) {
  data = { count: 0 };   // file missing or invalid -> start fresh
}

data.count += 1;
await writeFile('data.json', JSON.stringify(data, null, 2), 'utf8');
# Python
import json

try:
    with open('data.json', 'r', encoding='utf-8') as f:
        data = json.load(f)          # load() reads + parses in one step
except (FileNotFoundError, json.JSONDecodeError):
    data = {'count': 0}

data['count'] += 1
with open('data.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, indent=2)     # dump() serializes + writes
<?php
// PHP
$data = ['count' => 0];
if (is_readable('data.json')) {
    $decoded = json_decode(file_get_contents('data.json'), true);
    if (is_array($decoded)) {
        $data = $decoded;            // guard against null on bad JSON
    }
}

$data['count'] = ($data['count'] ?? 0) + 1;
file_put_contents('data.json', json_encode($data, JSON_PRETTY_PRINT));

βœ… Note the shortcuts

Python's json.load(f) / json.dump(data, f) fuse the file step and the JSON step into one call β€” handy, but under the hood it's still read-then-parse. Node and PHP keep the two steps visible, which makes the pipeline easy to see.

Encoding, Binary & Error Cases

Text vs binary

Text mode decodes bytes into characters using an encoding. Binary mode hands you the raw bytes untouched β€” use it for images, PDFs, or anything that isn't human-readable text.

# Python β€” 'rb' = read bytes; no encoding argument in binary mode
with open('logo.png', 'rb') as f:
    raw = f.read()          # a bytes object, not a str
// Node.js β€” omit the encoding to get a Buffer of raw bytes
import { readFile } from 'node:fs/promises';
const raw = await readFile('logo.png');   // a Buffer

Common error cases to handle

SituationNode.jsPythonPHP
File not foundthrows ENOENTFileNotFoundErrorwarning + false
No permissionthrows EACCESPermissionErrorwarning + false
Malformed JSONSyntaxErrorJSONDecodeErrorreturns null

⚠️ PHP fails quietly β€” check the return value

Unlike Node and Python, PHP's file_get_contents returns false (with a warning) on failure rather than throwing, and json_decode returns null on bad JSON. Always test the result: use is_readable() before reading, check for false, and confirm json_decode didn't return null (or pass the JSON_THROW_ON_ERROR flag to make it throw like the others).

<?php
// PHP β€” make json_decode throw instead of returning null
try {
    $data = json_decode($text, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    // now handled like Python / Node
}
πŸ’‘ Safe-write tip: To avoid a half-written file if the process dies mid-write, write to a temp file first and then rename it over the target. A rename is atomic on most filesystems, so readers never see a partial file.

Hands-on Exercise

πŸ‹οΈ Build a Visit Counter

Objective: Write a small program that maintains a visits.json file shaped like { "total": 0, "lastVisit": null }. On each run it should:

  1. Read and parse visits.json β€” but start from a default object if the file is missing or invalid.
  2. Increment total by 1 and set lastVisit to the current timestamp.
  3. Write the updated object back as pretty-printed JSON.
  4. Print the new total.

Implement it in one language of your choice first, then try a second for contrast.

πŸ’‘ Hint

Reuse the read–modify–write pattern from the round-trip section. The only additions are the timestamp (new Date().toISOString() in JS, datetime.now().isoformat() in Python, date('c') in PHP) and printing the total at the end. Wrap the read+parse in your error handling so the very first run β€” when the file doesn't exist yet β€” still works.

βœ… Solution
// Node.js β€” visits.js  (run with: node visits.js)
import { readFile, writeFile } from 'node:fs/promises';

let data;
try {
  data = JSON.parse(await readFile('visits.json', 'utf8'));
} catch {
  data = { total: 0, lastVisit: null };
}

data.total += 1;
data.lastVisit = new Date().toISOString();

await writeFile('visits.json', JSON.stringify(data, null, 2), 'utf8');
console.log(`Total visits: ${data.total}`);
# Python β€” visits.py  (run with: python visits.py)
import json
from datetime import datetime

try:
    with open('visits.json', 'r', encoding='utf-8') as f:
        data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
    data = {'total': 0, 'lastVisit': None}

data['total'] += 1
data['lastVisit'] = datetime.now().isoformat()

with open('visits.json', 'w', encoding='utf-8') as f:
    json.dump(data, f, indent=2)

print(f"Total visits: {data['total']}")
<?php
// PHP β€” visits.php  (run with: php visits.php)
$data = ['total' => 0, 'lastVisit' => null];
if (is_readable('visits.json')) {
    $decoded = json_decode(file_get_contents('visits.json'), true);
    if (is_array($decoded)) {
        $data = $decoded;
    }
}

$data['total'] = ($data['total'] ?? 0) + 1;
$data['lastVisit'] = date('c');

file_put_contents('visits.json', json_encode($data, JSON_PRETTY_PRINT));
echo "Total visits: {$data['total']}\n";

🎯 Quick Quiz

Question 1: Which pair of Node.js calls turns a JSON file into a usable object?

Question 2: In PHP, what does json_decode return when given malformed JSON (without the throw flag)?

Question 3: Why should you pass 'utf8' to Node's readFile (or encoding='utf-8' to Python's open) when reading text?

Summary & Quiz

πŸŽ‰ Key Takeaways

  • File work is two hops: bytes ↔ text (the file API) and text ↔ object (the JSON API) β€” keep them distinct in your head.
  • Node uses fs/promises (async, await) with JSON.parse/stringify; Python uses open with the json module; PHP uses file_get_contents/file_put_contents with json_decode/encode.
  • Always be explicit about UTF-8 encoding, and use binary mode for non-text files.
  • Handle the failure cases: Node and Python throw, but PHP returns false/null unless you pass JSON_THROW_ON_ERROR.
  • The workhorse pattern is read β†’ parse β†’ modify β†’ stringify β†’ write, guarded so a missing file just starts fresh.

πŸ“š Further Reading

πŸš€ What's Next?

You now have every language-level building block β€” data structures, error handling, and file/JSON I/O. Next you'll put them together in the Weekend Project: A Polyglot Mini-Server, building the same small server in each language to feel their differences first-hand.

πŸŽ‰ Nice work!

You can read, transform, and persist data in three languages. Time to build something real.