Working with JSON Data

JSON is like a universal language for computers to exchange information. Think of it as a neatly organized form that both humans can read and computers can process. It's like filling out a passport application - structured sections with labels and values.
**JSON** (JavaScript Object Notation) is a lightweight, text‑based data interchange format. It is easy for humans to read and write, and easy for machines to parse and generate. JSON is language‑independent but uses conventions familiar to programmers of the C family of languages, including Python. It has become the de facto standard for data exchange between web servers and clients (APIs), configuration files, and data storage.

## JSON vs. Python Dictionaries

JSON looks very similar to Python dictionaries, with a few differences:
- JSON keys **must** be strings enclosed in double quotes (`"key"`).
- JSON values can be strings (double quotes), numbers, booleans (`true`/`false`), null, arrays (`[]`), or objects (`{}`).
- In Python, booleans are `True`/`False`; in JSON they are `true`/`false`.
- JSON does not support tuples, sets, or custom objects directly.

## The `json` Module

Python's built‑in `json` module provides methods to convert between Python objects and JSON strings/files.

### Core Functions

- `json.dumps(obj, indent=None)` – serialises a Python object to a JSON **string**.
- `json.dump(obj, file, indent=None)` – writes JSON to a file.
- `json.loads(json_string)` – parses a JSON **string** into a Python object.
- `json.load(file)` – reads JSON from a file and converts it to a Python object.

### Python to JSON Conversion Table

| Python | JSON |
|--------|------|
| `dict` | object |
| `list`, `tuple` | array |
| `str` | string |
| `int`, `float` | number |
| `True` | `true` |
| `False` | `false` |
| `None` | `null` |

### Pretty Printing

Use the `indent` parameter to make JSON output readable:
```python
json.dumps(data, indent=2)
```

## Reading and Writing JSON Files

The typical workflow is:
1. Read JSON from a file using `json.load()`.
2. Modify the Python data.
3. Write back using `json.dump()`.

## Handling JSON with APIs

Most web APIs return data in JSON format. You can use the `requests` library to fetch the data, then `json.loads()` or `response.json()` to parse it.

## JSON Validation and Error Handling

`json.loads()` raises a `JSONDecodeError` if the string is not valid JSON. You can catch this to handle malformed data gracefully.

## Custom JSON Encoders

Sometimes you need to serialise non‑standard types like `datetime` or `set`. You can subclass `json.JSONEncoder` and override the `default` method.

## Real‑World Use Cases

- **Configuration files** – store app settings (e.g., `config.json`).
- **API communication** – sending/receiving data to/from REST APIs.
- **Data storage** – saving user preferences, game state, etc.
- **Data exchange** – between frontend (JavaScript) and backend (Python).
- **Logging** – storing structured logs.

## Common Mistakes

- Forgetting that JSON keys must be double‑quoted – `{'name': 'John'}` is not valid JSON, `{"name": "John"}` is.
- Using `json.dump()` with a file opened in text mode – it works, but ensure correct encoding.
- Not handling `JSONDecodeError` when parsing untrusted data.
- Mixing up `dump`/`dumps` and `load`/`loads`.
- Trying to serialise a custom object without a custom encoder.

## Practice Exercises

1. Convert a Python dictionary to a JSON string and back.
2. Read a JSON file containing a list of products, calculate the total price, and write a new JSON file with the total.
3. Fetch a public API (e.g., `https://api.github.com/users/octocat`) and parse the JSON response.
4. Create a configuration manager that reads settings from `config.json`, allows modification, and saves changes.
5. Write a custom JSON encoder to serialise a `datetime` object.

This lesson provides **10 complete examples** covering basic operations, file handling, processing JSON data, user input, API simulation, configuration files, validation, nested data, custom encoders, and a real‑world e‑commerce order.
# Working with JSON Data
import json
import pprint

print("WORKING WITH JSON DATA")
print("=" * 50)

# Example 1: Basic JSON operations
print("\n1. BASIC JSON OPERATIONS")
print("-" * 25)

# Sample data as Python dictionary
python_data = {
    "name": "John Doe",
    "age": 30,
    "city": "New York",
    "is_student": False,
    "courses": ["Math", "Science", "English"],
    "grades": {"Math": 90, "Science": 85, "English": 88},
    "contact": {
        "email": "john@example.com",
        "phone": "123-456-7890"
    }
}

print("Python dictionary:")
pprint.pprint(python_data, indent=2)

# Convert Python dictionary to JSON string
print("\nConvert to JSON string:")
json_string = json.dumps(python_data, indent=2)
print(json_string)

# Convert JSON string back to Python dictionary
print("\nConvert back to Python:")
python_dict_again = json.loads(json_string)
print(f"Name: {python_dict_again['name']}")
print(f"Age: {python_dict_again['age']}")
print(f"First course: {python_dict_again['courses'][0]}")

# Example 2: Reading and Writing JSON files
print("\n\n2. READING AND WRITING JSON FILES")
print("-" * 25)

# Write data to JSON file
student_data = {
    "students": [
        {
            "id": 1,
            "name": "Alice",
            "age": 20,
            "major": "Computer Science",
            "grades": {"math": 90, "science": 85, "english": 92}
        },
        {
            "id": 2,
            "name": "Bob",
            "age": 21,
            "major": "Mathematics",
            "grades": {"math": 95, "science": 88, "english": 80}
        },
        {
            "id": 3,
            "name": "Charlie",
            "age": 19,
            "major": "Physics",
            "grades": {"math": 85, "science": 92, "english": 78}
        }
    ],
    "class_name": "Introduction to Programming",
    "instructor": "Dr. Smith",
    "semester": "Spring 2024"
}

# Write to file
with open("students.json", "w") as file:
    json.dump(student_data, file, indent=2)

print("Created students.json file")

# Read from file
with open("students.json", "r") as file:
    loaded_data = json.load(file)

print("\nLoaded data from file:")
print(f"Class: {loaded_data['class_name']}")
print(f"Instructor: {loaded_data['instructor']}")
print(f"Number of students: {len(loaded_data['students'])}")

# Example 3: Processing JSON data
print("\n\n3. PROCESSING JSON DATA")
print("-" * 25)

print("Student Information:")
print("=" * 40)

for student in loaded_data["students"]:
    print(f"\nID: {student['id']}")
    print(f"Name: {student['name']}")
    print(f"Age: {student['age']}")
    print(f"Major: {student['major']}")
    
    # Calculate average grade
    grades = student["grades"]
    avg_grade = sum(grades.values()) / len(grades)
    print(f"Average Grade: {avg_grade:.1f}")
    
    # Find highest grade
    highest_subject = max(grades, key=grades.get)
    print(f"Best Subject: {highest_subject} ({grades[highest_subject]})")

# Example 4: Creating JSON from user input
print("\n\n4. CREATING JSON FROM USER INPUT")
print("-" * 25)

# Simulate user input
def create_student_profile():
    """Create student profile as JSON"""
    print("Create Student Profile")
    print("-" * 20)
    
    # In real program, use input()
    # For demo, using predefined values
    student = {
        "name": input("Enter name: ") or "Emma Wilson",
        "student_id": input("Enter student ID: ") or "S2024001",
        "email": input("Enter email: ") or "emma@university.edu",
        "enrollment_year": int(input("Enter enrollment year: ") or 2023),
        "active": True
    }
    
    # Add courses
    courses = []
    print("\nEnter courses (type 'done' when finished):")
    while True:
        course = input("Course name: ") or "Mathematics"
        if course.lower() == "done":
            break
        credit = int(input("Credits: ") or 3)
        courses.append({"name": course, "credits": credit})
    
    student["courses"] = courses
    
    # Convert to JSON
    student_json = json.dumps(student, indent=2)
    
    print("\nStudent Profile (JSON):")
    print(student_json)
    
    return student

# Create profile
student_profile = create_student_profile()

# Example 5: Working with APIs (simulated)
print("\n\n5. WORKING WITH APIS (SIMULATED)")
print("-" * 25)

# Simulate API response (in real life, you'd use requests library)
api_response_json = '''
{
  "weather": [
    {
      "id": 800,
      "main": "Clear",
      "description": "clear sky",
      "icon": "01d"
    }
  ],
  "main": {
    "temp": 22.5,
    "feels_like": 23.1,
    "temp_min": 20.0,
    "temp_max": 25.0,
    "pressure": 1012,
    "humidity": 65
  },
  "wind": {
    "speed": 3.6,
    "deg": 200
  },
  "name": "London",
  "dt": 1678886400
}
'''

# Parse API response
weather_data = json.loads(api_response_json)

print("Weather API Response:")
print("=" * 30)
print(f"City: {weather_data['name']}")
print(f"Temperature: {weather_data['main']['temp']}°C")
print(f"Feels like: {weather_data['main']['feels_like']}°C")
print(f"Weather: {weather_data['weather'][0]['description']}")
print(f"Humidity: {weather_data['main']['humidity']}%")
print(f"Wind Speed: {weather_data['wind']['speed']} m/s")

# Example 6: Config files in JSON
print("\n\n6. CONFIGURATION FILES")
print("-" * 25)

# Create config
app_config = {
    "app_name": "MyApp",
    "version": "1.0.0",
    "settings": {
        "theme": "dark",
        "language": "en",
        "notifications": True,
        "auto_save": True
    },
    "database": {
        "host": "localhost",
        "port": 5432,
        "username": "admin",
        "max_connections": 10
    },
    "features": ["user_auth", "file_upload", "reports", "analytics"]
}

# Save config
with open("config.json", "w") as config_file:
    json.dump(app_config, config_file, indent=2)

print("Created config.json")

# Load and use config
with open("config.json", "r") as config_file:
    config = json.load(config_file)

print(f"\nApp: {config['app_name']} v{config['version']}")
print(f"Theme: {config['settings']['theme']}")
print(f"Database: {config['database']['host']}:{config['database']['port']}")
print(f"Features: {', '.join(config['features'])}")

# Example 7: JSON validation
print("\n\n7. JSON VALIDATION AND ERROR HANDLING")
print("-" * 25)

# Invalid JSON strings
invalid_json_strings = [
    '{"name": "John", "age": 30,}',  # Trailing comma
    '{"name": "John", "age": }',     # Missing value
    '{"name": "John", age: 30}',     # Unquoted key
    '{"name": "John", "age": 30'     # Missing closing brace
]

print("Testing JSON validation:")
for i, json_str in enumerate(invalid_json_strings, 1):
    print(f"\nTest {i}: {json_str[:30]}...")
    try:
        data = json.loads(json_str)
        print("  ✓ Valid JSON")
    except json.JSONDecodeError as e:
        print(f"  ✗ Invalid JSON: {e.msg}")
        print(f"    Error at position: {e.pos}")

# Example 8: Complex nested JSON
print("\n\n8. COMPLEX NESTED JSON")
print("-" * 25)

# Company organizational structure
company_data = {
    "company": "TechCorp Inc.",
    "departments": [
        {
            "name": "Engineering",
            "manager": "Alice Johnson",
            "teams": [
                {
                    "name": "Frontend",
                    "members": ["Bob", "Charlie", "Diana"],
                    "projects": ["Website Redesign", "Mobile App"]
                },
                {
                    "name": "Backend",
                    "members": ["Eve", "Frank", "Grace"],
                    "projects": ["API Development", "Database Optimization"]
                }
            ]
        },
        {
            "name": "Sales",
            "manager": "Henry Smith",
            "teams": [
                {
                    "name": "Enterprise",
                    "members": ["Ivy", "Jack"],
                    "regions": ["North America", "Europe"]
                }
            ]
        }
    ]
}

# Process nested data
print(f"Company: {company_data['company']}")
print("\nDepartment Structure:")
for dept in company_data["departments"]:
    print(f"\n  Department: {dept['name']}")
    print(f"  Manager: {dept['manager']}")
    print(f"  Teams: {len(dept['teams'])}")
    
    for team in dept["teams"]:
        print(f"    - {team['name']}: {len(team['members'])} members")

# Example 9: Custom JSON encoder/decoder
print("\n\n9. CUSTOM JSON ENCODER")
print("-" * 25)

import datetime

class CustomJSONEncoder(json.JSONEncoder):
    """Custom encoder for special types"""
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return obj.isoformat()
        elif isinstance(obj, set):
            return list(obj)
        elif hasattr(obj, '__dict__'):
            return obj.__dict__
        return super().default(obj)

# Data with special types
complex_data = {
    "event": "Conference",
    "date": datetime.datetime(2024, 6, 15, 9, 0, 0),
    "attendees": {"Alice", "Bob", "Charlie"},
    "metadata": {"version": 1, "active": True}
}

print("Original data with special types:")
print(complex_data)

print("\nEncoded with custom encoder:")
encoded = json.dumps(complex_data, cls=CustomJSONEncoder, indent=2)
print(encoded)

# Example 10: Real-world scenario - E-commerce
print("\n\n10. REAL-WORLD: E-COMMERCE ORDER")
print("-" * 25)

order = {
    "order_id": "ORD20240315001",
    "customer": {
        "id": "CUST001",
        "name": "John Smith",
        "email": "john.smith@email.com",
        "address": {
            "street": "123 Main St",
            "city": "New York",
            "state": "NY",
            "zip": "10001"
        }
    },
    "items": [
        {"product_id": "P001", "name": "Laptop", "quantity": 1, "price": 999.99},
        {"product_id": "P002", "name": "Mouse", "quantity": 2, "price": 25.50},
        {"product_id": "P003", "name": "Keyboard", "quantity": 1, "price": 79.99}
    ],
    "payment": {
        "method": "credit_card",
        "transaction_id": "TXN789012",
        "amount": 1130.98,
        "status": "completed"
    },
    "shipping": {
        "method": "express",
        "tracking_number": "TRK123456789",
        "estimated_delivery": "2024-03-18"
    },
    "order_date": "2024-03-15T10:30:00",
    "status": "processing"
}

# Calculate total
items_total = sum(item["quantity"] * item["price"] for item in order["items"])
print(f"Order ID: {order['order_id']}")
print(f"Customer: {order['customer']['name']}")
print(f"\nItems:")
for item in order["items"]:
    total = item["quantity"] * item["price"]
    print(f"  {item['name']} x{item['quantity']}: ${total:.2f}")

print(f"\nSubtotal: ${items_total:.2f}")
print(f"Payment Amount: ${order['payment']['amount']:.2f}")
print(f"Status: {order['status'].upper()}")
print(f"Tracking: {order['shipping']['tracking_number']}")

# Save order to file
with open("order.json", "w") as f:
    json.dump(order, f, indent=2)
print("\nOrder saved to order.json")

→ Run this code interactively