Computing FoundationsAPIs and the Web › Day 24

Hands-on lab — Day 24: JSON and Data Serialization

Commands

Setup

cd labs/sections/computing-foundations/day-024-json-and-data-serialization

Run

bash examples/json_tools.sh
bash starter/json_tools.sh

Test

bash tests/run_tests.sh

File tree

examples/json_tools.sh
examples/samples/broken.json
examples/samples/config.json
expected-output/FIELDS.md
expected-output/json_tools.txt
expected-output/run_tests.txt
metadata.yml
README.md
requirements/README.md
security.md
starter/json_tools.sh
starter/json-worksheet.md
tests/run_tests.sh
troubleshooting.md

Lab README

Day 024 lab — Parse and Build JSON

Lesson

Purpose

Day 24's lesson explains serialization and JSON in depth. This lab makes it concrete and offline: you validate a valid nested JSON file, extract a nested field, pretty-print, and watch a deliberately broken file get rejected with a real parser error — using only python3, which is already on your machine. This is the exact groundwork for Week 4's project, a weather command-line dashboard that parses live JSON.

Learning objectives

  • Validate a JSON file from the command line and read the pass/fail result.
  • Extract a nested field and an array element from a JSON document.
  • Pretty-print JSON so a human can read its structure.
  • Recognize the error a trailing comma produces and explain why it is invalid.
  • Compare the python3 and jq ways of doing the same JSON task.

Prerequisites

  • The Day 24 lesson (read it first — it explains every concept this lab uses).
  • A terminal with python3 available (macOS and Linux ship it).
  • No programming experience required; every command is given and explained.

Supported operating systems

  • macOS — fully supported (tested on macOS with Apple Silicon, Python 3.14).
  • Linux — fully supported (any distribution with python3 and bash).
  • Windows — run the scripts inside WSL, or run the individual python3 commands from the lesson by hand with Python 3 for Windows.

Hardware requirements

Any computer made in roughly the last 15 years. The lab only reads two small text files; it needs no special RAM, disk, or GPU.

Required software

  • python3 (3.x) — for python3 -m json.tool and the json module.
  • bash (3.2 or newer) — preinstalled on macOS and Linux.

Free and open-source options

Everything here is free and open source: python3, bash, and the optional jq are all open source or ship with your OS. No account, API key, network, or purchase is needed.

Installation

None. Clone the repository (or copy this directory) and you are ready:

cd labs/sections/computing-foundations/day-024-json-and-data-serialization

File structure

day-024-json-and-data-serialization/
├── README.md                          ← you are here
├── metadata.yml                       ← machine-readable lab metadata
├── starter/
│   ├── json_tools.sh                  ← YOUR working file (4 exercises)
│   └── json-worksheet.md              ← record your findings here
├── examples/
│   ├── json_tools.sh                  ← completed reference implementation
│   └── samples/
│       ├── config.json                ← a valid, nested JSON file
│       └── broken.json                ← a deliberately broken JSON file
├── tests/
│   └── run_tests.sh                   ← automated checks (7 checks)
├── expected-output/
│   ├── json_tools.txt                 ← real captured run of the example
│   ├── run_tests.txt                  ← real captured test run
│   └── FIELDS.md                      ← what must hold on every platform
├── requirements/
│   └── README.md                      ← dependency statement (python3 only)
├── troubleshooting.md
└── security.md

How to run

From this directory:

## 1. See the finished result first
bash examples/json_tools.sh

## 2. Your task: work through the four exercises in the starter
bash starter/json_tools.sh

## 3. Check your work
bash tests/run_tests.sh

What the commands do

  • bash examples/json_tools.sh — the reference script. It validates config.json with python3 -m json.tool, extracts coordinates.lat and sensors[0] with python3 one-liners (showing the jq equivalent as a comment), counts the top-level keys, pretty-prints the file, and then proves broken.json is rejected by capturing and printing its real parser error.
  • bash starter/json_tools.sh — the same four skills as four numbered exercises for you to work through: validate, extract a nested field, pretty-print, and spot the error in the broken file.
  • bash tests/run_tests.sh — runs seven checks with no network: python3 is present, the valid sample parses, the broken sample is rejected, coordinates.lat extracts to 26.9124, the file has 7 top-level keys, sensors[0] is temperature, and the example script runs end to end.

Expected output

See expected-output/json_tools.txt — a real captured run. The broken file is rejected with (on Python 3.14):

Illegal trailing comma before end of object: line 4 column 53 (char 97)

On Python 3.12 and older the wording is Expecting property name enclosed in double quotes instead — same mistake, same non-zero exit. See expected-output/FIELDS.md for what must hold on every platform.

Validation steps

  1. Run bash examples/json_tools.sh — it must finish without an unexpected error and print the broken-file message under step 5.
  2. Confirm the extraction prints 26.9124 and nothing else.
  3. Confirm the key count is 7.
  4. Run the tests (next section) — all checks must pass.

Tests

bash tests/run_tests.sh

Expected final line: 7 checks, 0 failure(s). The command exits 0 on success and non-zero on any failure, so it can run in CI. No network is used.

Cleanup

Nothing to clean up: the scripts only read the two sample files and write only console output. To reset your starter work, restore it from git: git checkout -- starter/json_tools.sh.

Troubleshooting

See troubleshooting.md for the full list (missing python3, key errors, quoting the one-liner, version-dependent error wording, optional jq).

Security notes

See security.md. Short version: the scripts run no network calls, need no elevated privileges, and only read local sample files — and the golden rule of the day is to parse untrusted JSON, never to eval it.

Extension exercises

  1. Serialize a value you build in Python and confirm the round trip: python3 -c "import json; d={'a':1,'b':[True,None]}; s=json.dumps(d); print(s); print(json.loads(s)==d)".
  2. Try to serialize a value JSON has no type for, such as a set (python3 -c "import json; print(json.dumps({1,2,3}))"), read the error, and note why sets and dates must be encoded as one of the six JSON types.
  3. If you installed jq, redo every extraction with jq and compare its output with the python3 version.
  • Previous day: Day 23 — REST Fundamentals: Resources and Verbs (labs/sections/computing-foundations/day-023-rest-fundamentals-resources-and-verbs/).
  • Next day: Day 25 — API Authentication: Keys, Tokens, and OAuth (labs/sections/computing-foundations/day-025-api-authentication-keys-tokens-and-oauth/).

Expected output

FIELDS.md

# Expected output — Day 024 lab

The two `.txt` files in this directory are real captured runs on the
authoring machine (macOS, Apple Silicon, Python 3.14.0, 2026-07-12):

- `json_tools.txt` — a full run of `bash examples/json_tools.sh`.
- `run_tests.txt` — a full run of `bash tests/run_tests.sh` (ends in
  `7 checks, 0 failure(s).`).

## What must be true on every platform

Regardless of OS or Python version, a correct run shows:

1. `examples/samples/config.json` **validates cleanly** (exit 0) and
   pretty-prints with 7 top-level keys.
2. Extracting `coordinates.lat` prints exactly `26.9124`.
3. Extracting `sensors[0]` prints exactly `temperature`.
4. `examples/samples/broken.json` **fails to validate** with a non-zero
   exit status.
5. `bash tests/run_tests.sh` ends with `7 checks, 0 failure(s).` and
   exits 0.

## The one platform-dependent line

The wording of the broken-file error depends on the Python version:

- **Python 3.13 and newer** (as captured here):
  `Illegal trailing comma before end of object: line 4 column 53 (char 97)`
- **Python 3.12 and older**:
  `Expecting property name enclosed in double quotes: line 5 column 1 (char ...)`

Both messages report the same underlying mistake — the trailing comma after
the `sensors` array — and both cause a non-zero exit, which is what the test
checks. The exact character offset is not asserted anywhere.

json_tools.txt

=== 1. Validate the good file ===
OK: examples/samples/config.json is valid JSON

=== 2. Extract a nested field (coordinates.lat) ===
26.9124

=== 3. Extract the first sensor (sensors[0]) and count top-level keys ===
temperature
7 top-level keys

=== 4. Pretty-print the good file ===
{
    "station": "Nimbus-7",
    "active": true,
    "elevation_m": 512,
    "coordinates": {
        "lat": 26.9124,
        "lon": 75.7873
    },
    "sensors": [
        "temperature",
        "humidity",
        "pressure"
    ],
    "calibration": {
        "temperature": {
            "offset": -0.4,
            "unit": "celsius"
        },
        "last_checked": "2026-07-01"
    },
    "notes": null
}

=== 5. The broken file is rejected ===
As expected, examples/samples/broken.json is invalid JSON. Parser said:
    Illegal trailing comma before end of object: line 4 column 53 (char 97)

Done. The good file round-trips; the broken file (a trailing comma) is refused.

run_tests.txt

  ok: python3 is available
  ok: valid sample parses cleanly
  ok: broken sample is rejected
  ok: coordinates.lat extracts to 26.9124
  ok: sample has 7 top-level keys
  ok: sensors[0] extracts to 'temperature'
  ok: examples/json_tools.sh runs successfully

7 checks, 0 failure(s).

Source files

examples/json_tools.sh (2146 bytes)
#!/usr/bin/env bash
# Day 024 lab — completed reference: work with JSON using only python3.
#
# Demonstrates the four skills the starter asks you to practise:
#   1. validate a JSON file            (python3 -m json.tool)
#   2. extract a nested field          (python3 one-liner; jq equivalent shown)
#   3. pretty-print a JSON file        (python3 -m json.tool)
#   4. show a broken file being rejected, with its real error
#
# Everything runs offline. Run from the lab directory:
#   bash examples/json_tools.sh
set -euo pipefail

good="examples/samples/config.json"
broken="examples/samples/broken.json"

echo "=== 1. Validate the good file ==="
# `python3 -m json.tool` parses its input and reprints it; it fails loudly
# (non-zero exit) if the input is not valid JSON.
if python3 -m json.tool "${good}" > /dev/null; then
  echo "OK: ${good} is valid JSON"
else
  echo "UNEXPECTED: ${good} failed to parse" >&2
  exit 1
fi
echo

echo "=== 2. Extract a nested field (coordinates.lat) ==="
# Python one-liner: load the file, walk into coordinates, print lat.
python3 -c "import json; print(json.load(open('${good}'))['coordinates']['lat'])"
# jq equivalent (if you have jq installed):
#   jq '.coordinates.lat' examples/samples/config.json
echo

echo "=== 3. Extract the first sensor (sensors[0]) and count top-level keys ==="
python3 -c "import json; d=json.load(open('${good}')); print(d['sensors'][0]); print(len(d), 'top-level keys')"
# jq equivalent:
#   jq '.sensors[0]' examples/samples/config.json
#   jq 'keys | length' examples/samples/config.json
echo

echo "=== 4. Pretty-print the good file ==="
python3 -m json.tool "${good}"
echo

echo "=== 5. The broken file is rejected ==="
# We EXPECT this to fail. Capture the error and show it, then continue.
if python3 -m json.tool "${broken}" > /dev/null 2> /tmp/day024_err.txt; then
  echo "UNEXPECTED: ${broken} parsed but should be invalid" >&2
  exit 1
else
  echo "As expected, ${broken} is invalid JSON. Parser said:"
  sed 's/^/    /' /tmp/day024_err.txt
  rm -f /tmp/day024_err.txt
fi
echo
echo "Done. The good file round-trips; the broken file (a trailing comma) is refused."
examples/samples/broken.json (101 bytes)
{
  "station": "Nimbus-7",
  "active": true,
  "sensors": ["temperature", "humidity", "pressure"],
}
examples/samples/config.json (317 bytes)
{
  "station": "Nimbus-7",
  "active": true,
  "elevation_m": 512,
  "coordinates": {
    "lat": 26.9124,
    "lon": 75.7873
  },
  "sensors": ["temperature", "humidity", "pressure"],
  "calibration": {
    "temperature": { "offset": -0.4, "unit": "celsius" },
    "last_checked": "2026-07-01"
  },
  "notes": null
}
metadata.yml (570 bytes)
lesson_id: D024
day: 24
kind: data-processing
languages: [bash]
setup_commands:
  - cd labs/sections/computing-foundations/day-024-json-and-data-serialization
run_commands:
  - bash examples/json_tools.sh
  - bash starter/json_tools.sh
test_commands:
  - bash tests/run_tests.sh
cleanup_commands:
  - 'git checkout -- starter/json_tools.sh  # optional: reset your work'
requires_network: false
requires_api_key: false
estimated_minutes: 30
last_executed: '2026-07-12'
executed_on: 'macOS (Apple Silicon), Python 3.14.0, bash tests/run_tests.sh → 7 checks, 0 failures'
requirements/README.md (1240 bytes)
# Dependencies — Day 024 lab

**Only `python3`, which is preinstalled on macOS and every mainstream Linux
distribution.** This lab is deliberately offline and dependency-light:

- `python3` (3.x) — provides `python3 -m json.tool` (validate and
  pretty-print) and the `json` module used by the one-line extractors. No
  third-party packages are installed.
- `bash` (3.2 or newer) — runs the lab scripts. Preinstalled on macOS and
  Linux.

## Optional

- `jq` — a dedicated command-line JSON processor. The lab's scripts show the
  `jq` equivalent of each `python3` step as a comment, so you can compare the
  two styles, but **every required step uses `python3` only**. Install `jq`
  from your package manager if you want to try the commented commands
  (`brew install jq` on macOS, `apt install jq` on Debian/Ubuntu).

## Platform notes

- **macOS / Linux:** nothing to install.
- **Windows:** run the scripts inside WSL (Windows Subsystem for Linux) so
  `bash` and `python3` behave as documented, or install Python 3 for Windows
  and run the individual `python3` commands from the lesson by hand.
- If `python3` is not found but `python` is, and `python --version` reports
  3.x, substitute `python` for `python3` throughout.
starter/json_tools.sh (2206 bytes)
#!/usr/bin/env bash
# Day 024 lab — YOUR working file. Complete the four exercises below.
#
# The completed reference version is in examples/json_tools.sh — try each
# exercise yourself first, then compare. Everything runs offline with
# python3 (preinstalled on macOS and Linux). Run from the lab directory:
#   bash starter/json_tools.sh
#
# Each exercise names the exact command to use in its comment. The commands
# below are already filled in so the script runs; work through them one at a
# time, changing the keys and files to explore, and check your understanding
# against examples/json_tools.sh.
set -euo pipefail

good="examples/samples/config.json"
broken="examples/samples/broken.json"

echo "=== Exercise 1: validate the good file ==="
# Use: python3 -m json.tool "${good}"
# It reprints the file if valid and fails with a non-zero exit if not.
# --- your command below ---
python3 -m json.tool "${good}"
echo

echo "=== Exercise 2: extract a nested field (coordinates.lat) ==="
# Use a python3 one-liner that loads the file and prints coordinates -> lat.
# Template (fill in the two keys):
#   python3 -c "import json; print(json.load(open('${good}'))['coordinates']['lat'])"
# jq equivalent (optional): jq '.coordinates.lat' examples/samples/config.json
# --- your command below ---
python3 -c "import json; print(json.load(open('${good}'))['coordinates']['lat'])"
echo

echo "=== Exercise 3: pretty-print the good file ==="
# json.tool already pretty-prints. Print it indented to the screen.
# Use: python3 -m json.tool "${good}"
# --- your command below ---
python3 -m json.tool "${good}"
echo

echo "=== Exercise 4: spot the error in the broken file ==="
# Validate the BROKEN file. It SHOULD fail — read the error it prints.
# Use: python3 -m json.tool "${broken}"
# (We wrap it so the script does not stop on the expected failure.)
# --- your command below ---
if python3 -m json.tool "${broken}" > /dev/null 2>&1; then
  echo "Hmm — broken.json parsed, but it should be invalid. Check the file."
else
  echo "Correct: broken.json is invalid. Full error message:"
  python3 -m json.tool "${broken}" || true
fi
echo
echo "Now record your findings in starter/json-worksheet.md."
starter/json-worksheet.md (1496 bytes)
# JSON worksheet — Day 024

Fill this in from your own runs of `starter/json_tools.sh` and the commands
in the lesson's hands-on section. Keep it — Week 4's project (a weather
command-line dashboard) parses live JSON and builds on this groundwork.

## The sample file (`examples/samples/config.json`)

| Question | Your answer | Command you used |
| --- | --- | --- |
| How many top-level keys does it have? |  |  |
| One nested value, and the path to it |  |  |
| What value type is `active`? |  | (read it — no command needed) |
| What value type is `sensors`? |  |  |
| What value type is `notes`? |  |  |

Hint for the key count: `python3 -c "import json; print(len(json.load(open('examples/samples/config.json'))))"`

Hint for a nested value: `python3 -c "import json; print(json.load(open('examples/samples/config.json'))['coordinates']['lat'])"`

## The broken file (`examples/samples/broken.json`)

Run `python3 -m json.tool examples/samples/broken.json` and record the exact
error message it prints:

```text
(paste the error line here)
```

**Which single character makes the document invalid?**

(one sentence)

**Why does that one character make the whole document invalid?**

(two or three sentences, in your own words)

## The six JSON value types

List all six value types JSON allows, and next to each write which key in
`config.json` is an example of it (some types appear more than once).

1. object —
2. array —
3. string —
4. number —
5. boolean —
6. null —
tests/run_tests.sh (2638 bytes)
#!/usr/bin/env bash
# Tests for the Day 024 lab. Run from the lab directory:
#   bash tests/run_tests.sh
#
# Verifies real behavior with no network:
#   - the valid sample parses cleanly (exit 0)
#   - the broken sample fails to parse (non-zero exit)
#   - field extraction returns the known value (coordinates.lat == 26.9124)
#   - the sample has the expected number of top-level keys (7)
#   - the completed example script runs end to end
set -u

lab_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${lab_dir}"
good="examples/samples/config.json"
broken="examples/samples/broken.json"
failures=0
checks=0

check() {
  local label="$1" ok="$2"
  checks=$((checks + 1))
  if [ "${ok}" = "yes" ]; then
    echo "  ok: ${label}"
  else
    echo "  FAIL: ${label}"
    failures=$((failures + 1))
  fi
}

# python3 must be available (the whole lab depends on it).
if command -v python3 > /dev/null 2>&1; then
  check "python3 is available" "yes"
else
  check "python3 is available" "no"
  echo
  echo "${checks} checks, ${failures} failure(s)."
  exit 1
fi

# 1. Valid sample parses (exit 0).
if python3 -m json.tool "${good}" > /dev/null 2>&1; then
  check "valid sample parses cleanly" "yes"
else
  check "valid sample parses cleanly" "no"
fi

# 2. Broken sample fails to parse (non-zero exit).
if python3 -m json.tool "${broken}" > /dev/null 2>&1; then
  check "broken sample is rejected" "no"
else
  check "broken sample is rejected" "yes"
fi

# 3. Field extraction returns the known nested value.
lat="$(python3 -c "import json; print(json.load(open('${good}'))['coordinates']['lat'])" 2>/dev/null)"
if [ "${lat}" = "26.9124" ]; then
  check "coordinates.lat extracts to 26.9124" "yes"
else
  check "coordinates.lat extracts to 26.9124 (got '${lat}')" "no"
fi

# 4. Known top-level key count.
keys="$(python3 -c "import json; print(len(json.load(open('${good}'))))" 2>/dev/null)"
if [ "${keys}" = "7" ]; then
  check "sample has 7 top-level keys" "yes"
else
  check "sample has 7 top-level keys (got '${keys}')" "no"
fi

# 5. Known first array element.
first="$(python3 -c "import json; print(json.load(open('${good}'))['sensors'][0])" 2>/dev/null)"
if [ "${first}" = "temperature" ]; then
  check "sensors[0] extracts to 'temperature'" "yes"
else
  check "sensors[0] extracts to 'temperature' (got '${first}')" "no"
fi

# 6. The completed example script runs end to end.
if bash examples/json_tools.sh > /dev/null 2>&1; then
  check "examples/json_tools.sh runs successfully" "yes"
else
  check "examples/json_tools.sh runs successfully" "no"
fi

echo
echo "${checks} checks, ${failures} failure(s)."
[ "${failures}" -eq 0 ]

Troubleshooting

Troubleshooting — Day 024 lab

python3: command not found

Your system may expose Python 3 as python. Check with python --version; if it reports 3.x, substitute python for python3 in every command. On Windows, run the lab inside WSL, or install Python 3 from python.org.

bash examples/json_tools.sh stops immediately with a JSON error

If it fails on config.json, that file has probably been edited by accident. Restore it from version control (git checkout -- examples/samples/config.json) and re-run. The script is designed so that only broken.json should fail.

The broken-file error wording is different from the README

That is expected. Python 3.13 and newer say Illegal trailing comma before end of object; older versions say Expecting property name enclosed in double quotes. Both report the same trailing-comma mistake and both exit non-zero, which is all the test checks. See expected-output/FIELDS.md.

KeyError when extracting a field

You asked for a key that is not in the file, or spelled one wrong. Keys are case-sensitive: use lat, not Lat, and coordinates, not Coordinates. Pretty-print the file first (python3 -m json.tool examples/samples/config.json) to see the exact key names.

The Python one-liner errors about quotes

Keep the outer double quotes around the -c program and single quotes for the filename inside, exactly as shown: python3 -c "import json; print(json.load(open('examples/samples/config.json'))['coordinates']['lat'])". Mixing them up makes the shell mangle the program before Python sees it.

jq: command not found

jq is optional. Every required step uses python3; the jq lines in the scripts are comments for comparison only. Install jq (brew install jq or apt install jq) only if you want to try them.

Permission denied when running a script

Run it through bash explicitly: bash starter/json_tools.sh. You do not need to mark the file executable.

Security notes

Security notes — Day 024 lab

  • What the scripts do: read two small local JSON files (examples/samples/config.json and examples/samples/broken.json) and print or validate them with python3. They make no network connections, write no files outside their own console output, and change no settings.
  • Never eval untrusted JSON. Because JSON's syntax overlaps with JavaScript's, it is tempting to turn a JSON string into values by running it as code — this is dangerous: a malicious payload could then execute arbitrary instructions. Always use a real parser (python3 -m json.tool, Python's json.load, or jq), which treats the input strictly as data and can only ever produce the six JSON value types, never executable behavior.
  • This lab only reads local sample files that ship with it, so there is no untrusted input here — but the habit matters for every real service you will call later, where the JSON comes from outside and must be parsed, not evaluated, and then validated before you trust its fields.
  • Privileges: everything runs as your normal user. Nothing needs sudo.
  • Reading before running: the scripts are short and commented — read them first. Running unread scripts is one of the most common ways developers get compromised.