Skip to main content

πŸ“‹ Lesson 8.2: Working with JSON

In the last lesson, you learned to read and write text files. But text files are just raw strings β€” they don't understand structure. If you want to save a user profile with a name, age, and list of hobbies, a plain text file forces you to invent your own format and write your own parser. JSON (JavaScript Object Notation) solves this problem. It's a universal format for structured data that every programming language can read and write β€” and it's the backbone of almost every web API on the internet.

🎯 Learning Objectives

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

  • Explain what JSON is and why it's the standard for data exchange
  • Read JSON syntax β€” objects, arrays, strings, numbers, booleans, and null
  • Parse JSON β€” convert a JSON string into a native data structure
  • Create JSON β€” convert a native data structure into a JSON string
  • Read and write JSON files to persist structured data
  • Navigate nested JSON β€” access deeply nested values
  • Choose between JSON and CSV for different use cases
  • Avoid common JSON pitfalls β€” trailing commas, single quotes, and more

Estimated Time: 45 minutes

Project: Build a contact book that saves to a JSON file

πŸ“‘ In This Lesson

What Is JSON?

JSON stands for JavaScript Object Notation. Despite the name, it's not just for JavaScript β€” it's a language-independent text format that every modern programming language supports. JSON is used for:

flowchart LR A["APIs
Web services send
and receive JSON"] --> B["Config Files
Settings stored
as JSON"] B --> C["Data Storage
Structured data
saved to disk"] C --> D["Data Exchange
Programs share
data via JSON"] style A fill:#3b82f6,color:#fff style B fill:#6366f1,color:#fff style C fill:#22c55e,color:#fff style D fill:#f59e0b,color:#fff

Here's what JSON looks like β€” if you've ever seen a JavaScript object or a Python dictionary, this will feel familiar:

{
    "name": "Alice Chen",
    "age": 28,
    "isStudent": false,
    "courses": ["Python 101", "Data Science", "Web Dev"],
    "address": {
        "city": "Portland",
        "state": "OR"
    },
    "gpa": null
}

That single block of text describes a person with a name, age, enrollment status, a list of courses, a nested address, and a missing GPA. Any programming language can read this and turn it into native data structures (dictionaries, objects, lists, arrays) β€” and any language can produce it too. That's what makes JSON the universal language of data exchange.

πŸŽ“ Instructor Note: Delivery Guidance

Students who've been working through this course have already seen JSON-like structures in Python dictionaries (lesson 14) and JavaScript objects. Point that out: "You already know how to read this!" The key new concept is that JSON is a text format β€” it's a string that looks like code but isn't. The act of converting between a JSON string and a live data structure (parsing/serializing) is the core skill of this lesson. Show a real-world JSON response from a public API in the browser (try https://api.github.com/users/github) to demonstrate that this isn't just academic β€” it's how the modern web works.

JSON Syntax Rules

JSON is strict about its format β€” stricter than Python or JavaScript. Here are the rules:

JSON Data Types

JSON Type Example 🐍 Python Equivalent ⚑ JS Equivalent πŸ”· C# Equivalent
String "hello" str string string
Number 42, 3.14 int / float number int / double
Boolean true, false True, False true, false true, false
Null null None null null
Object {"key": "value"} dict object Dictionary / class
Array [1, 2, 3] list Array List / array

Syntax Rules That Trip People Up

# These are PYTHON dictionaries β€” NOT valid JSON!
# Python is more relaxed than JSON.

python_dict = {
    'name': 'Alice',        # ❌ JSON requires DOUBLE quotes, not single
    "age": 28,              # βœ… Double quotes are fine
    "active": True,         # ❌ JSON uses lowercase: true, not True
    "score": None,          # ❌ JSON uses null, not None
    "tags": ["a", "b",],    # ❌ JSON does NOT allow trailing commas
}

# The VALID JSON equivalent would be:
valid_json_string = '''
{
    "name": "Alice",
    "age": 28,
    "active": true,
    "score": null,
    "tags": ["a", "b"]
}
'''
# Notice:
# - ALL strings use DOUBLE quotes (keys AND values)
# - Booleans are lowercase: true/false
# - null instead of None
# - NO trailing comma after the last item
# - NO comments allowed in JSON!
// These are JAVASCRIPT objects β€” NOT valid JSON!
// JavaScript is more relaxed than JSON.

let jsObject = {
    name: "Alice",          // ❌ JSON requires quotes around keys
    'age': 28,              // ❌ JSON requires DOUBLE quotes, not single
    "active": true,         // βœ… This one is actually valid JSON
    "score": undefined,     // ❌ JSON has no undefined β€” use null
    "tags": ["a", "b",],   // ❌ JSON does NOT allow trailing commas
};

// The VALID JSON equivalent would be:
let validJsonString = `
{
    "name": "Alice",
    "age": 28,
    "active": true,
    "score": null,
    "tags": ["a", "b"]
}
`;
// Notice:
// - ALL keys must be in DOUBLE quotes
// - ALL strings use DOUBLE quotes
// - No undefined β€” use null
// - NO trailing comma after the last item
// - NO comments allowed in JSON!
// - NO single quotes anywhere
// C# doesn't have JSON-like literal syntax for objects,
// but here are the JSON rules to remember:

// βœ… VALID JSON:
string validJson = @"
{
    ""name"": ""Alice"",
    ""age"": 28,
    ""active"": true,
    ""score"": null,
    ""tags"": [""a"", ""b""]
}
";

// ❌ INVALID JSON β€” common mistakes:
// { name: "Alice" }           β€” keys must be quoted
// { "name": 'Alice' }         β€” must use double quotes, not single
// { "active": True }          β€” must be lowercase true/false
// { "tags": ["a", "b", ] }   β€” no trailing comma
// { "name": "Alice", /* comment */ }  β€” no comments!

// C# note: In a verbatim string (@"..."), you escape
// double quotes by doubling them: "" instead of \"

⚠️ The 5 Strict Rules of JSON

  1. Double quotes only β€” for both keys and string values (no single quotes)
  2. No trailing commas β€” the last item in an object or array must NOT have a comma after it
  3. No comments β€” JSON has no comment syntax at all
  4. Lowercase booleans β€” true and false, not True/False
  5. null, not None/undefined β€” the JSON keyword is null

Parsing JSON (String β†’ Data)

Parsing (also called deserializing) converts a JSON string into a native data structure you can work with in your language β€” dictionaries in Python, objects in JavaScript, or typed objects in C#.

flowchart LR A["JSON String
'{\"name\": \"Alice\"}'"] -->|"Parse /
Deserialize"| B["Native Object
{name: 'Alice'}"] style A fill:#f59e0b,color:#fff style B fill:#22c55e,color:#fff
import json

# --- Parse a JSON string into a Python dictionary ---
json_string = '{"name": "Alice", "age": 28, "courses": ["Python", "SQL"]}'

data = json.loads(json_string)   # loads = "load string"
print(type(data))     # <class 'dict'>
print(data["name"])   # Alice
print(data["age"])    # 28
print(data["courses"])  # ['Python', 'SQL']
print(data["courses"][0])  # Python

# --- JSON types β†’ Python types ---
json_example = '''
{
    "text": "hello",
    "number": 42,
    "decimal": 3.14,
    "flag": true,
    "nothing": null,
    "items": [1, 2, 3],
    "nested": {"a": 1}
}
'''
result = json.loads(json_example)
print(type(result["text"]))     # str
print(type(result["number"]))   # int
print(type(result["decimal"]))  # float
print(type(result["flag"]))     # bool (True)
print(type(result["nothing"]))  # NoneType (None)
print(type(result["items"]))    # list
print(type(result["nested"]))   # dict

# --- Handling invalid JSON ---
try:
    bad = json.loads('{"name": "Alice",}')   # Trailing comma!
except json.JSONDecodeError as e:
    print(f"Invalid JSON: {e}")
    # Invalid JSON: Expecting property name enclosed in double quotes: line 1 column 18
// --- Parse a JSON string into a JavaScript object ---
let jsonString = '{"name": "Alice", "age": 28, "courses": ["Python", "SQL"]}';

let data = JSON.parse(jsonString);
console.log(typeof data);       // "object"
console.log(data.name);         // Alice
console.log(data.age);          // 28
console.log(data.courses);      // ['Python', 'SQL']
console.log(data.courses[0]);   // Python

// --- JSON types β†’ JavaScript types ---
let jsonExample = `
{
    "text": "hello",
    "number": 42,
    "decimal": 3.14,
    "flag": true,
    "nothing": null,
    "items": [1, 2, 3],
    "nested": {"a": 1}
}
`;
let result = JSON.parse(jsonExample);
console.log(typeof result.text);      // "string"
console.log(typeof result.number);    // "number"
console.log(typeof result.decimal);   // "number" (JS has only one number type)
console.log(typeof result.flag);      // "boolean"
console.log(result.nothing);          // null
console.log(Array.isArray(result.items));  // true
console.log(typeof result.nested);    // "object"

// --- Handling invalid JSON ---
try {
    let bad = JSON.parse('{"name": "Alice",}');  // Trailing comma!
} catch (err) {
    console.log(`Invalid JSON: ${err.message}`);
    // Invalid JSON: Expected double-quoted property name in JSON at position 17
}

// Fun fact: JSON was inspired by JavaScript object syntax,
// which is why JSON.parse and JSON.stringify are built-in!
using System.Text.Json;

// --- Method 1: Parse to JsonDocument (dynamic, no class needed) ---
string jsonString = @"{""name"": ""Alice"", ""age"": 28, ""courses"": [""Python"", ""SQL""]}";

JsonDocument doc = JsonDocument.Parse(jsonString);
JsonElement root = doc.RootElement;

Console.WriteLine(root.GetProperty("name").GetString());   // Alice
Console.WriteLine(root.GetProperty("age").GetInt32());      // 28
Console.WriteLine(root.GetProperty("courses")[0].GetString()); // Python

// --- Method 2: Deserialize to a typed class (recommended!) ---
class Student
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
    public List<string> Courses { get; set; } = new();
}

var options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true    // "name" matches Name
};

Student? student = JsonSerializer.Deserialize<Student>(jsonString, options);
Console.WriteLine(student?.Name);         // Alice
Console.WriteLine(student?.Age);          // 28
Console.WriteLine(student?.Courses[0]);   // Python

// --- Method 3: Deserialize to Dictionary (flexible) ---
var dict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(jsonString);
Console.WriteLine(dict?["name"].GetString());  // Alice

// --- Handling invalid JSON ---
try
{
    JsonDocument.Parse(@"{""name"": ""Alice"",}");  // Trailing comma!
}
catch (JsonException ex)
{
    Console.WriteLine($"Invalid JSON: {ex.Message}");
}

πŸ’‘ "Parse" vs. "Load" β€” Same Thing, Different Names

  • Python: json.loads(string) β€” "load string"
  • JavaScript: JSON.parse(string)
  • C#: JsonSerializer.Deserialize<T>(string) or JsonDocument.Parse(string)

They all do the same thing: take a JSON string and turn it into native data structures. The word "deserialize" is the formal term β€” it means converting serialized (text) data back into live objects.

Creating JSON (Data β†’ String)

Serializing (or stringifying) is the reverse: take a native data structure and convert it into a JSON string, ready to save to a file or send over a network.

flowchart LR A["Native Object
{name: 'Alice'}"] -->|"Stringify /
Serialize"| B["JSON String
'{\"name\": \"Alice\"}'"] style A fill:#22c55e,color:#fff style B fill:#f59e0b,color:#fff
import json

# --- Convert Python dict β†’ JSON string ---
student = {
    "name": "Alice",
    "age": 28,
    "courses": ["Python", "SQL"],
    "active": True,       # Python True β†’ JSON true (automatic!)
    "gpa": None,          # Python None β†’ JSON null (automatic!)
}

json_string = json.dumps(student)   # dumps = "dump string"
print(json_string)
# {"name": "Alice", "age": 28, "courses": ["Python", "SQL"], "active": true, "gpa": null}

# --- Pretty-printed JSON (human readable) ---
pretty = json.dumps(student, indent=4)
print(pretty)
# {
#     "name": "Alice",
#     "age": 28,
#     "courses": [
#         "Python",
#         "SQL"
#     ],
#     "active": true,
#     "gpa": null
# }

# --- Other useful options ---
# Sort keys alphabetically:
sorted_json = json.dumps(student, indent=2, sort_keys=True)

# Handle non-ASCII characters:
data = {"city": "ZΓΌrich", "greeting": "こんにけは"}
print(json.dumps(data))                          # Escaped: "\u00fc", "\u3053..."
print(json.dumps(data, ensure_ascii=False))       # Raw: "ZΓΌrich", "こんにけは"

# --- What CANNOT be converted to JSON ---
import datetime
try:
    json.dumps({"now": datetime.datetime.now()})
except TypeError as e:
    print(f"Error: {e}")
    # Error: Object of type datetime is not JSON serializable
    # Fix: convert to string first: str(datetime.datetime.now())
// --- Convert JavaScript object β†’ JSON string ---
let student = {
    name: "Alice",
    age: 28,
    courses: ["Python", "SQL"],
    active: true,
    gpa: null,
};

let jsonString = JSON.stringify(student);
console.log(jsonString);
// {"name":"Alice","age":28,"courses":["Python","SQL"],"active":true,"gpa":null}

// --- Pretty-printed JSON ---
let pretty = JSON.stringify(student, null, 4);
console.log(pretty);
// {
//     "name": "Alice",
//     "age": 28,
//     ...
// }

// The second argument is a "replacer" β€” use null to include everything.
// The third argument is the indent (spaces or a string).

// --- Replacer: control which properties are included ---
let filtered = JSON.stringify(student, ["name", "age"], 2);
console.log(filtered);
// {
//   "name": "Alice",
//   "age": 28
// }

// --- What CANNOT be converted to JSON ---
let problematic = {
    func: function() {},     // Functions are DROPPED silently!
    undef: undefined,        // undefined is DROPPED silently!
    date: new Date(),        // Dates become strings (ISO format)
    regex: /abc/,            // RegExp becomes {}
};

console.log(JSON.stringify(problematic, null, 2));
// {
//   "date": "2026-04-13T10:30:00.000Z",
//   "regex": {}
// }
// Notice: func and undef are completely gone!
using System.Text.Json;

// --- Method 1: Serialize from a class ---
class Student
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
    public List<string> Courses { get; set; } = new();
    public bool Active { get; set; }
    public double? Gpa { get; set; }    // nullable β†’ JSON null
}

var student = new Student
{
    Name = "Alice",
    Age = 28,
    Courses = new List<string> { "Python", "SQL" },
    Active = true,
    Gpa = null
};

string jsonString = JsonSerializer.Serialize(student);
Console.WriteLine(jsonString);
// {"Name":"Alice","Age":28,"Courses":["Python","SQL"],"Active":true,"Gpa":null}

// --- Pretty-printed JSON ---
var options = new JsonSerializerOptions { WriteIndented = true };
string pretty = JsonSerializer.Serialize(student, options);
Console.WriteLine(pretty);

// --- Use camelCase keys (common for JSON) ---
var camelOptions = new JsonSerializerOptions
{
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
Console.WriteLine(JsonSerializer.Serialize(student, camelOptions));
// {
//   "name": "Alice",
//   "age": 28,
//   ...
// }

// --- Method 2: Serialize from Dictionary ---
var dict = new Dictionary<string, object?>
{
    ["name"] = "Alice",
    ["age"] = 28,
    ["active"] = true,
    ["gpa"] = null,
};
Console.WriteLine(JsonSerializer.Serialize(dict, options));

βœ… Serialize / Stringify Quick Reference

  • Python: json.dumps(data, indent=4)
  • JavaScript: JSON.stringify(data, null, 4)
  • C#: JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true })

The indent parameter makes JSON human-readable. Use it for config files and debugging. Omit it for compact JSON (API responses, data transfer).

Reading and Writing JSON Files

The real power of JSON: saving structured data to files and loading it back. This combines the file skills from Lesson 22 with the JSON skills from this lesson.

import json
from pathlib import Path

# --- Write JSON to a file ---
students = [
    {"name": "Alice", "age": 28, "gpa": 3.9},
    {"name": "Bob", "age": 22, "gpa": 3.2},
    {"name": "Charlie", "age": 25, "gpa": 3.7},
]

# Method 1: json.dump() writes directly to a file
with open("students.json", "w", encoding="utf-8") as file:
    json.dump(students, file, indent=4)
    # Note: dump() not dumps() β€” no 's' means "to file"

# Method 2: Pathlib
Path("students.json").write_text(
    json.dumps(students, indent=4),
    encoding="utf-8"
)

# --- Read JSON from a file ---
# Method 1: json.load() reads directly from a file
with open("students.json", "r", encoding="utf-8") as file:
    loaded = json.load(file)   # load() not loads()

print(loaded[0]["name"])  # Alice

# Method 2: Pathlib
loaded = json.loads(Path("students.json").read_text(encoding="utf-8"))

# --- The pattern: Load β†’ Modify β†’ Save ---
# This is the most common JSON file workflow:
with open("students.json", "r") as f:
    data = json.load(f)                        # 1. Load

data.append({"name": "Diana", "age": 24, "gpa": 3.5})  # 2. Modify

with open("students.json", "w") as f:
    json.dump(data, f, indent=4)               # 3. Save
const fs = require("fs");

// --- Write JSON to a file ---
let students = [
    { name: "Alice", age: 28, gpa: 3.9 },
    { name: "Bob", age: 22, gpa: 3.2 },
    { name: "Charlie", age: 25, gpa: 3.7 },
];

// Stringify with indent, then write
fs.writeFileSync("students.json", JSON.stringify(students, null, 4));

// --- Read JSON from a file ---
let jsonText = fs.readFileSync("students.json", "utf-8");
let loaded = JSON.parse(jsonText);

console.log(loaded[0].name);  // Alice

// --- The pattern: Load β†’ Modify β†’ Save ---
let data = JSON.parse(fs.readFileSync("students.json", "utf-8"));  // 1. Load
data.push({ name: "Diana", age: 24, gpa: 3.5 });                  // 2. Modify
fs.writeFileSync("students.json", JSON.stringify(data, null, 4));  // 3. Save

// --- Helper functions (common pattern) ---
function loadJSON(filename) {
    if (!fs.existsSync(filename)) return null;
    return JSON.parse(fs.readFileSync(filename, "utf-8"));
}

function saveJSON(filename, data) {
    fs.writeFileSync(filename, JSON.stringify(data, null, 4));
}

// Usage:
let students2 = loadJSON("students.json") || [];
students2.push({ name: "Eve", age: 23, gpa: 3.8 });
saveJSON("students.json", students2);
using System.IO;
using System.Text.Json;

class Student
{
    public string Name { get; set; } = "";
    public int Age { get; set; }
    public double Gpa { get; set; }
}

var options = new JsonSerializerOptions
{
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};

// --- Write JSON to a file ---
var students = new List<Student>
{
    new() { Name = "Alice", Age = 28, Gpa = 3.9 },
    new() { Name = "Bob", Age = 22, Gpa = 3.2 },
    new() { Name = "Charlie", Age = 25, Gpa = 3.7 },
};

string json = JsonSerializer.Serialize(students, options);
File.WriteAllText("students.json", json);

// --- Read JSON from a file ---
string jsonText = File.ReadAllText("students.json");
var readOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var loaded = JsonSerializer.Deserialize<List<Student>>(jsonText, readOptions);

Console.WriteLine(loaded?[0].Name);  // Alice

// --- The pattern: Load β†’ Modify β†’ Save ---
string raw = File.ReadAllText("students.json");
var data = JsonSerializer.Deserialize<List<Student>>(raw, readOptions) ?? new();
data.Add(new Student { Name = "Diana", Age = 24, Gpa = 3.5 });    // Modify
File.WriteAllText("students.json", JsonSerializer.Serialize(data, options)); // Save

πŸ’‘ Python Naming Tip: load vs. loads

  • json.load(file) β€” load from a file object
  • json.loads(string) β€” load from a string (the s = "string")
  • json.dump(data, file) β€” dump to a file object
  • json.dumps(data) β€” dump to a string

This naming convention is unique to Python. JS uses JSON.parse()/JSON.stringify() for strings, and you combine them with readFileSync/writeFileSync for files.

πŸŽ“ Instructor Note: Delivery Guidance

The Load β†’ Modify β†’ Save pattern is the most important takeaway from this section. This is how real applications work: they read a JSON config or data file, make changes in memory, and write it back. Have students actually create a students.json file, run the code to add a student, then open the file to see it changed. That feedback loop β€” code changes a file on disk, you see the result β€” is very satisfying for beginners. The Python load vs loads naming is a common source of confusion; the mnemonic "s = string" helps. In C#, emphasize PropertyNameCaseInsensitive = true because JSON typically uses camelCase but C# properties use PascalCase.

Nested JSON and Complex Structures

Real-world JSON is almost always nested β€” objects inside objects, arrays of objects, arrays inside objects. Here's how to navigate deep structures:

import json

# A realistic JSON structure (imagine this from an API response)
company_json = '''
{
    "company": "TechCorp",
    "founded": 2015,
    "departments": [
        {
            "name": "Engineering",
            "head": "Alice Chen",
            "employees": [
                {"name": "Bob", "role": "Backend", "skills": ["Python", "SQL"]},
                {"name": "Carol", "role": "Frontend", "skills": ["React", "CSS"]}
            ]
        },
        {
            "name": "Marketing",
            "head": "Dave Kim",
            "employees": [
                {"name": "Eve", "role": "Content", "skills": ["Writing", "SEO"]}
            ]
        }
    ]
}
'''

data = json.loads(company_json)

# --- Navigating the structure ---
print(data["company"])                             # "TechCorp"
print(data["departments"][0]["name"])               # "Engineering"
print(data["departments"][0]["employees"][1]["name"])  # "Carol"
print(data["departments"][0]["employees"][0]["skills"][0])  # "Python"

# --- Iterating over nested structures ---
for dept in data["departments"]:
    print(f"\n{dept['name']} (Head: {dept['head']})")
    for emp in dept["employees"]:
        skills = ", ".join(emp["skills"])
        print(f"  β€’ {emp['name']} β€” {emp['role']} ({skills})")

# Output:
# Engineering (Head: Alice Chen)
#   β€’ Bob β€” Backend (Python, SQL)
#   β€’ Carol β€” Frontend (React, CSS)
# Marketing (Head: Dave Kim)
#   β€’ Eve β€” Content (Writing, SEO)

# --- Safe access for missing keys ---
# Use .get() to avoid KeyError:
phone = data.get("phone", "N/A")         # "N/A" β€” key doesn't exist
dept0 = data.get("departments", [])[0]   # Safe even if departments missing
let companyJson = `
{
    "company": "TechCorp",
    "founded": 2015,
    "departments": [
        {
            "name": "Engineering",
            "head": "Alice Chen",
            "employees": [
                {"name": "Bob", "role": "Backend", "skills": ["Python", "SQL"]},
                {"name": "Carol", "role": "Frontend", "skills": ["React", "CSS"]}
            ]
        },
        {
            "name": "Marketing",
            "head": "Dave Kim",
            "employees": [
                {"name": "Eve", "role": "Content", "skills": ["Writing", "SEO"]}
            ]
        }
    ]
}
`;

let data = JSON.parse(companyJson);

// --- Navigating the structure ---
console.log(data.company);                           // "TechCorp"
console.log(data.departments[0].name);               // "Engineering"
console.log(data.departments[0].employees[1].name);  // "Carol"
console.log(data.departments[0].employees[0].skills[0]); // "Python"

// --- Iterating over nested structures ---
for (let dept of data.departments) {
    console.log(`\n${dept.name} (Head: ${dept.head})`);
    for (let emp of dept.employees) {
        console.log(`  β€’ ${emp.name} β€” ${emp.role} (${emp.skills.join(", ")})`);
    }
}

// --- Safe access with optional chaining (?.) ---
// If any part of the chain is null/undefined, returns undefined instead of crashing
console.log(data?.departments?.[0]?.employees?.[0]?.name);   // "Bob"
console.log(data?.departments?.[5]?.name);                    // undefined (no crash!)
console.log(data?.phone ?? "N/A");                            // "N/A"

// Optional chaining (?.) is one of JavaScript's best features for JSON!
using System.Text.Json;

// --- Using JsonDocument for dynamic navigation ---
string companyJson = @"
{
    ""company"": ""TechCorp"",
    ""founded"": 2015,
    ""departments"": [
        {
            ""name"": ""Engineering"",
            ""head"": ""Alice Chen"",
            ""employees"": [
                {""name"": ""Bob"", ""role"": ""Backend"", ""skills"": [""Python"", ""SQL""]},
                {""name"": ""Carol"", ""role"": ""Frontend"", ""skills"": [""React"", ""CSS""]}
            ]
        },
        {
            ""name"": ""Marketing"",
            ""head"": ""Dave Kim"",
            ""employees"": [
                {""name"": ""Eve"", ""role"": ""Content"", ""skills"": [""Writing"", ""SEO""]}
            ]
        }
    ]
}";

JsonDocument doc = JsonDocument.Parse(companyJson);
JsonElement root = doc.RootElement;

// --- Navigating the structure ---
Console.WriteLine(root.GetProperty("company").GetString());
Console.WriteLine(root.GetProperty("departments")[0].GetProperty("name").GetString());
Console.WriteLine(root.GetProperty("departments")[0]
    .GetProperty("employees")[1]
    .GetProperty("name").GetString());   // "Carol"

// --- Iterating over nested structures ---
foreach (JsonElement dept in root.GetProperty("departments").EnumerateArray())
{
    string name = dept.GetProperty("name").GetString()!;
    string head = dept.GetProperty("head").GetString()!;
    Console.WriteLine($"\n{name} (Head: {head})");

    foreach (JsonElement emp in dept.GetProperty("employees").EnumerateArray())
    {
        string empName = emp.GetProperty("name").GetString()!;
        string role = emp.GetProperty("role").GetString()!;
        var skills = emp.GetProperty("skills").EnumerateArray()
            .Select(s => s.GetString());
        Console.WriteLine($"  β€’ {empName} β€” {role} ({string.Join(", ", skills)})");
    }
}

// --- Safe access with TryGetProperty ---
if (root.TryGetProperty("phone", out JsonElement phoneEl))
    Console.WriteLine(phoneEl.GetString());
else
    Console.WriteLine("N/A");  // Key doesn't exist

πŸ’‘ Reading Nested JSON: Think Like a Map

Read the access chain left to right, like following a map:

  • data["departments"] β†’ get the "departments" array
  • [0] β†’ first department (Engineering)
  • ["employees"] β†’ get its employees array
  • [1] β†’ second employee (Carol)
  • ["name"] β†’ get her name β†’ "Carol"

If you get lost in nested JSON, use pretty-printing (json.dumps(data, indent=2)) to visualize the structure, or paste it into an online JSON formatter.

JSON vs. CSV

You now know two data formats. When should you use which?

Feature CSV JSON
Structure Flat β€” rows and columns only Nested β€” objects, arrays, any depth
Data Types Everything is a string Strings, numbers, booleans, null, arrays, objects
Human Readable Easy in spreadsheets Easy with formatting; dense without
File Size Smaller (less overhead) Larger (keys repeated for every record)
Best For Tabular data (spreadsheets, databases) APIs, configs, nested/complex data
Spreadsheet-Friendly Yes β€” opens directly in Excel/Sheets No β€” needs conversion

βœ… Quick Decision Guide

  • Use CSV when your data is a flat table (rows of the same type with the same columns) β€” student grades, sales records, sensor readings
  • Use JSON when your data has nesting, mixed types, or irregular structure β€” user profiles, API responses, configuration files, game save states
  • Rule of thumb: If it fits neatly in a spreadsheet β†’ CSV. If it doesn't β†’ JSON.

Common JSON Pitfalls

import json

# --- Pitfall 1: Single quotes ---
bad = "{'name': 'Alice'}"       # ❌ Python-style, not JSON!
# json.loads(bad)  β†’ JSONDecodeError
good = '{"name": "Alice"}'      # βœ… Double quotes
data = json.loads(good)

# --- Pitfall 2: Trailing comma ---
bad = '{"a": 1, "b": 2,}'      # ❌ Trailing comma after 2
# json.loads(bad)  β†’ JSONDecodeError
good = '{"a": 1, "b": 2}'      # βœ… No trailing comma

# --- Pitfall 3: Forgetting that JSON values are strings ---
data = json.loads('{"age": "28"}')
# data["age"] is the STRING "28", not the number 28!
# age = data["age"] + 1  β†’ TypeError!
age = int(data["age"]) + 1      # βœ… Convert first

# --- Pitfall 4: Trying to serialize non-JSON types ---
from datetime import datetime

data = {"created": datetime.now()}
# json.dumps(data)  β†’ TypeError: datetime is not serializable

# Fix: convert to string first
data = {"created": datetime.now().isoformat()}
print(json.dumps(data))  # {"created": "2026-04-13T10:30:00.123456"}

# --- Pitfall 5: Not handling missing keys ---
data = {"name": "Alice"}
# print(data["age"])  β†’ KeyError!
print(data.get("age", "unknown"))  # βœ… "unknown"
// --- Pitfall 1: Confusing JS objects with JSON strings ---
let obj = { name: "Alice" };        // JS object (live data)
let str = '{"name": "Alice"}';      // JSON string (text)

// You parse STRINGS, not objects:
// JSON.parse(obj)  β†’ ERROR β€” it's already an object!
// JSON.stringify(str) β†’ Wraps the string in MORE quotes!

// --- Pitfall 2: undefined disappears ---
let data = { name: "Alice", nickname: undefined };
console.log(JSON.stringify(data));
// {"name":"Alice"}  ← nickname is GONE!
// Fix: use null instead of undefined if you want it in JSON

// --- Pitfall 3: Date objects become strings ---
let event = { name: "Launch", date: new Date("2026-04-13") };
let json = JSON.stringify(event);
let parsed = JSON.parse(json);
console.log(typeof parsed.date);  // "string" β€” NOT a Date object!
// Fix: convert back manually:
parsed.date = new Date(parsed.date);

// --- Pitfall 4: Circular references ---
let a = {};
let b = { ref: a };
a.ref = b;                          // a β†’ b β†’ a β†’ b β†’ ...
// JSON.stringify(a)  β†’ TypeError: circular structure!
// Fix: remove circular refs, or use a library like 'flatted'

// --- Pitfall 5: NaN and Infinity ---
console.log(JSON.stringify({ val: NaN }));       // {"val":null}
console.log(JSON.stringify({ val: Infinity }));  // {"val":null}
// NaN and Infinity become null in JSON β€” they're not valid JSON numbers!
using System.Text.Json;

// --- Pitfall 1: Case sensitivity ---
// JSON:  {"name": "Alice"}   (camelCase)
// C#:    public string Name  (PascalCase)
// Without PropertyNameCaseInsensitive, "name" won't match Name!

var options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true   // βœ… Always set this!
};

// --- Pitfall 2: Null handling ---
string jsonWithNull = @"{""name"": ""Alice"", ""age"": null}";

class Person
{
    public string Name { get; set; } = "";
    public int Age { get; set; }  // ❌ int can't be null!
}
// Fix: use nullable types
class PersonFixed
{
    public string Name { get; set; } = "";
    public int? Age { get; set; }  // βœ… int? can be null
}

// --- Pitfall 3: Strict deserialization ---
// System.Text.Json is STRICT by default.
// Extra properties in JSON are silently ignored β€” that's fine.
// But missing REQUIRED properties can cause issues.

// --- Pitfall 4: DateTime formatting ---
var data = new { Created = DateTime.Now };
Console.WriteLine(JsonSerializer.Serialize(data));
// {"Created":"2026-04-13T10:30:00.1234567"}
// ISO 8601 format β€” this is correct and standard!
// But when deserializing, make sure the format matches.

// --- Pitfall 5: Escaped quotes in verbatim strings ---
// In @"..." strings, double the quotes:
string json = @"{""name"": ""Alice""}";  // βœ…
// Or use $ with regular strings:
string name = "Alice";
string json2 = $"{{\"name\": \"{name}\"}}";  // βœ… But messy!
// Best: build objects and serialize, don't hand-write JSON strings
πŸŽ“ Instructor Note: Delivery Guidance

The pitfalls section is designed as a troubleshooting reference. Don't try to cover every pitfall in lecture β€” instead, focus on the top 2–3 for each language. For Python: single quotes and serializing datetime. For JavaScript: undefined disappearing and dates becoming strings. For C#: case sensitivity and null handling. The others will be encountered naturally during exercises. Consider having students deliberately trigger each error to see the error message β€” recognizing "JSONDecodeError" or "TypeError: circular structure" helps students debug faster when they hit these issues in their own code.

Exercises

πŸ‹οΈ Exercise 1: JSON Explorer

Objective: Given the following JSON string, write code to extract and print specific pieces of information.

{
    "library": "City Central Library",
    "books": [
        {
            "title": "The Great Gatsby",
            "author": "F. Scott Fitzgerald",
            "year": 1925,
            "genres": ["Fiction", "Classic"],
            "available": true
        },
        {
            "title": "Clean Code",
            "author": "Robert C. Martin",
            "year": 2008,
            "genres": ["Programming", "Software Engineering"],
            "available": false
        },
        {
            "title": "Dune",
            "author": "Frank Herbert",
            "year": 1965,
            "genres": ["Science Fiction", "Classic"],
            "available": true
        }
    ],
    "location": {
        "city": "Portland",
        "state": "OR",
        "zip": "97201"
    }
}

Extract and print:

  1. The library name
  2. The title of the second book
  3. The first genre of the third book
  4. The city from the location
  5. All available book titles (loop through and filter)
  6. The total number of books
βœ… Solution
import json

json_string = '''{ ... }'''  # (paste the full JSON above)
data = json.loads(json_string)

# 1. Library name
print(data["library"])                          # City Central Library

# 2. Title of the second book
print(data["books"][1]["title"])                 # Clean Code

# 3. First genre of the third book
print(data["books"][2]["genres"][0])             # Science Fiction

# 4. City from location
print(data["location"]["city"])                  # Portland

# 5. All available book titles
for book in data["books"]:
    if book["available"]:
        print(f"  Available: {book['title']}")
# Available: The Great Gatsby
# Available: Dune

# 6. Total number of books
print(f"Total books: {len(data['books'])}")     # Total books: 3
let jsonString = `{ ... }`;  // (paste the full JSON above)
let data = JSON.parse(jsonString);

console.log(data.library);                    // City Central Library
console.log(data.books[1].title);             // Clean Code
console.log(data.books[2].genres[0]);         // Science Fiction
console.log(data.location.city);              // Portland

data.books
    .filter(b => b.available)
    .forEach(b => console.log(`  Available: ${b.title}`));

console.log(`Total books: ${data.books.length}`);
var doc = JsonDocument.Parse(jsonString);
var root = doc.RootElement;

Console.WriteLine(root.GetProperty("library").GetString());
Console.WriteLine(root.GetProperty("books")[1].GetProperty("title").GetString());
Console.WriteLine(root.GetProperty("books")[2].GetProperty("genres")[0].GetString());
Console.WriteLine(root.GetProperty("location").GetProperty("city").GetString());

foreach (var book in root.GetProperty("books").EnumerateArray())
{
    if (book.GetProperty("available").GetBoolean())
        Console.WriteLine($"  Available: {book.GetProperty("title").GetString()}");
}

Console.WriteLine($"Total books: {root.GetProperty("books").GetArrayLength()}");

πŸ‹οΈ Exercise 2: Contact Book

Objective: Build a command-line contact book that stores contacts in a JSON file. This extends the note-taking app from Lesson 22 with structured data.

Features:

  1. Add a contact: Name, phone number, and email
  2. List all contacts: Display formatted contact list
  3. Search contacts: Find by name (case-insensitive)
  4. Delete a contact: Remove by name
  5. All data persists in contacts.json

Sample contacts.json:

[
    {
        "name": "Alice Chen",
        "phone": "555-0101",
        "email": "alice@example.com"
    },
    {
        "name": "Bob Smith",
        "phone": "555-0202",
        "email": "bob@example.com"
    }
]
βœ… Solution
import json
from pathlib import Path

CONTACTS_FILE = "contacts.json"

def load_contacts():
    path = Path(CONTACTS_FILE)
    if not path.exists():
        return []
    return json.loads(path.read_text(encoding="utf-8"))

def save_contacts(contacts):
    Path(CONTACTS_FILE).write_text(
        json.dumps(contacts, indent=4),
        encoding="utf-8"
    )

def add_contact():
    name = input("Name: ").strip()
    phone = input("Phone: ").strip()
    email = input("Email: ").strip()

    if not name:
        print("Name is required.")
        return

    contacts = load_contacts()
    contacts.append({"name": name, "phone": phone, "email": email})
    save_contacts(contacts)
    print(f"Added {name}!")

def list_contacts():
    contacts = load_contacts()
    if not contacts:
        print("No contacts yet.")
        return

    print(f"\n--- Contacts ({len(contacts)}) ---")
    for i, c in enumerate(contacts, 1):
        print(f"  {i}. {c['name']}")
        print(f"     Phone: {c['phone']}")
        print(f"     Email: {c['email']}")
    print()

def search_contacts():
    query = input("Search name: ").strip().lower()
    if not query:
        return

    contacts = load_contacts()
    matches = [c for c in contacts if query in c["name"].lower()]

    if not matches:
        print("No matches found.")
    else:
        print(f"\n--- Found {len(matches)} match(es) ---")
        for c in matches:
            print(f"  {c['name']} | {c['phone']} | {c['email']}")
    print()

def delete_contact():
    name = input("Delete contact named: ").strip().lower()
    contacts = load_contacts()
    original_count = len(contacts)
    contacts = [c for c in contacts if c["name"].lower() != name]

    if len(contacts) == original_count:
        print("Contact not found.")
    else:
        save_contacts(contacts)
        print("Contact deleted.")

# --- Main loop ---
while True:
    print("Contacts: [A]dd | [L]ist | [S]earch | [D]elete | [Q]uit")
    choice = input("> ").strip().lower()

    if choice == "a": add_contact()
    elif choice == "l": list_contacts()
    elif choice == "s": search_contacts()
    elif choice == "d": delete_contact()
    elif choice == "q":
        print("Goodbye!")
        break
    else:
        print("Invalid choice.")
const fs = require("fs");
const readline = require("readline");
const CONTACTS_FILE = "contacts.json";

const rl = readline.createInterface({
    input: process.stdin, output: process.stdout
});
const prompt = (q) => new Promise(r => rl.question(q, r));

function loadContacts() {
    if (!fs.existsSync(CONTACTS_FILE)) return [];
    return JSON.parse(fs.readFileSync(CONTACTS_FILE, "utf-8"));
}

function saveContacts(contacts) {
    fs.writeFileSync(CONTACTS_FILE, JSON.stringify(contacts, null, 4));
}

async function addContact() {
    let name = (await prompt("Name: ")).trim();
    let phone = (await prompt("Phone: ")).trim();
    let email = (await prompt("Email: ")).trim();
    if (!name) { console.log("Name is required."); return; }

    let contacts = loadContacts();
    contacts.push({ name, phone, email });
    saveContacts(contacts);
    console.log(`Added ${name}!`);
}

function listContacts() {
    let contacts = loadContacts();
    if (contacts.length === 0) { console.log("No contacts yet."); return; }
    console.log(`\n--- Contacts (${contacts.length}) ---`);
    contacts.forEach((c, i) => {
        console.log(`  ${i+1}. ${c.name}\n     Phone: ${c.phone}\n     Email: ${c.email}`);
    });
}

async function searchContacts() {
    let query = (await prompt("Search name: ")).trim().toLowerCase();
    let matches = loadContacts().filter(c => c.name.toLowerCase().includes(query));
    if (matches.length === 0) console.log("No matches.");
    else matches.forEach(c => console.log(`  ${c.name} | ${c.phone} | ${c.email}`));
}

async function deleteContact() {
    let name = (await prompt("Delete named: ")).trim().toLowerCase();
    let contacts = loadContacts();
    let filtered = contacts.filter(c => c.name.toLowerCase() !== name);
    if (filtered.length === contacts.length) console.log("Not found.");
    else { saveContacts(filtered); console.log("Deleted."); }
}

async function main() {
    while (true) {
        let ch = (await prompt("Contacts: [A]dd [L]ist [S]earch [D]elete [Q]uit\n> ")).trim().toLowerCase();
        if (ch === "a") await addContact();
        else if (ch === "l") listContacts();
        else if (ch === "s") await searchContacts();
        else if (ch === "d") await deleteContact();
        else if (ch === "q") { console.log("Goodbye!"); rl.close(); break; }
    }
}
main();
using System.Text.Json;
const string ContactsFile = "contacts.json";
var jsonOpts = new JsonSerializerOptions { WriteIndented = true };

List<Dictionary<string, string>> LoadContacts()
{
    if (!File.Exists(ContactsFile)) return new();
    string json = File.ReadAllText(ContactsFile);
    return JsonSerializer.Deserialize<List<Dictionary<string, string>>>(json) ?? new();
}

void SaveContacts(List<Dictionary<string, string>> contacts)
    => File.WriteAllText(ContactsFile, JsonSerializer.Serialize(contacts, jsonOpts));

void AddContact()
{
    Console.Write("Name: "); string name = Console.ReadLine()?.Trim() ?? "";
    Console.Write("Phone: "); string phone = Console.ReadLine()?.Trim() ?? "";
    Console.Write("Email: "); string email = Console.ReadLine()?.Trim() ?? "";
    if (string.IsNullOrEmpty(name)) { Console.WriteLine("Name required."); return; }
    var contacts = LoadContacts();
    contacts.Add(new() { ["name"] = name, ["phone"] = phone, ["email"] = email });
    SaveContacts(contacts);
    Console.WriteLine($"Added {name}!");
}

void ListContacts()
{
    var contacts = LoadContacts();
    if (contacts.Count == 0) { Console.WriteLine("No contacts."); return; }
    for (int i = 0; i < contacts.Count; i++)
        Console.WriteLine($"  {i+1}. {contacts[i]["name"]}\n     Phone: {contacts[i]["phone"]}\n     Email: {contacts[i]["email"]}");
}

while (true)
{
    Console.WriteLine("Contacts: [A]dd [L]ist [S]earch [D]elete [Q]uit");
    Console.Write("> ");
    string? ch = Console.ReadLine()?.Trim().ToLower();
    switch (ch)
    {
        case "a": AddContact(); break;
        case "l": ListContacts(); break;
        case "q": Console.WriteLine("Goodbye!"); return;
    }
}

πŸ‹οΈ Exercise 3: Config File Manager

Objective: Many programs use JSON for configuration. Write a program that:

  1. Creates a default config.json if it doesn't exist (with default settings like theme, language, font size)
  2. Reads and displays the current settings
  3. Lets the user change a setting by name and value
  4. Saves the updated config back to the file

Sample config.json:

{
    "theme": "dark",
    "language": "en",
    "fontSize": 14,
    "autoSave": true,
    "maxRecentFiles": 10
}
πŸ’‘ Hints

Use the Load β†’ Modify β†’ Save pattern. When the user changes a setting, you'll need to convert the value from a string input to the appropriate type (number, boolean, or keep as string). You can check if the value looks like a number (isdigit()), or if it's "true"/"false".

πŸŽ“ Instructor Note: Delivery Guidance

Exercise 1 (JSON Explorer) is a warmup β€” it practices accessing nested data without the complexity of file I/O. Exercise 2 (Contact Book) is the main project β€” it's a direct evolution of the note-taking app from Lesson 22, upgrading from raw text to structured JSON. Students should compare the two and see how JSON makes the data much easier to search, filter, and display. Exercise 3 (Config File Manager) introduces a real-world use case β€” nearly every app they'll build or use has a config file. The type conversion challenge (string input β†’ correct type) is a good stretch goal. If time is short, prioritize Exercise 2.

Summary

πŸŽ‰ Key Takeaways

  • JSON is the universal text format for structured data β€” supported by every language, used by every API
  • JSON syntax is strict: double quotes only, no trailing commas, no comments, lowercase booleans, null not None
  • Parsing converts a JSON string β†’ native data structures (dicts, objects, arrays)
  • Serializing converts native data β†’ a JSON string; use indent for readability
  • Load β†’ Modify β†’ Save is the standard JSON file workflow
  • Nested JSON is navigated with chained access: data["key"][0]["nested"]
  • JSON vs. CSV: Use CSV for flat tables, JSON for nested/complex/mixed-type data
  • Always handle errors: invalid JSON, missing keys, wrong types

Cross-Language Comparison

Operation 🐍 Python ⚑ JavaScript πŸ”· C#
Parse string json.loads(s) JSON.parse(s) JsonSerializer.Deserialize<T>(s)
Stringify json.dumps(d, indent=4) JSON.stringify(d, null, 4) JsonSerializer.Serialize(d)
Read from file json.load(file) JSON.parse(readFileSync()) Deserialize(ReadAllText())
Write to file json.dump(d, file) writeFileSync(stringify()) WriteAllText(Serialize())
Safe key access d.get("key", default) d?.key ?? default TryGetProperty()
Module/namespace import json Built-in JSON System.Text.Json

πŸš€ What's Next?

You can now save and load structured data with JSON. In the next lesson, we'll use JSON in its most important context: making API requests. You'll learn to fetch live data from the internet β€” weather, news, user profiles β€” and process the JSON responses in your programs. This is where your code starts talking to the rest of the world.

🎯 Quick Check

Question 1: Which of these is valid JSON?

Question 2: What does Python's json.loads() do?

Question 3: When should you use JSON instead of CSV?