Chapter 6 taught you JSON fundamentals: calling .json(), navigating dictionaries,
and extracting data from clean APIs like Random User. That foundation works when
APIs are consistent and predictable.
Production APIs are neither.
This chapter teaches you to handle the messy reality: deeply nested structures, optional fields that appear and disappear, the same business concept arriving in different shapes across endpoints. You'll build systematic approaches that work regardless of how APIs structure their responses.
Issue 1: "Fields nest several levels deep"
Here's a realistic Orders API response. Can you find where city is hiding?
{
"order": {
"id": "ord_9f2c",
"customer": {
"profile": {
"name": {
"first": "Ava",
"last": "Murphy"
},
"contact": {
"email": "ava@example.com",
"address": {
"shipping": {
"line1": "12 Harbour St",
"city": "Dublin",
"country": "IE"
}
}
}
}
},
"items": [
{
"sku": "SKU-001",
"product": {
"details": {
"title": "Stainless Water Bottle",
"pricing": {
"currency": "EUR",
"amount": 24.99
}
}
}
}
],
"payment": {
"provider": "stripe",
"transaction": {
"status": "succeeded",
"risk": {
"score": 12,
"flags": ["ip_mismatch"]
}
}
}
}
}
Challenge: What's the complete path to reach "Dublin"?
Hint: order.customer.____.contact.____.shipping.city
order.customer.profile.contact.address.shipping.city
That's 7 levels deep. Each dot means "go one level deeper into the object" until you reach the final value.
Issue 2: "Optional sections appear and disappear"
Real APIs often return optional sections only when they exist. For example, if a customer hasn't saved a shipping address yet,
the address block might be missing entirely.
In this response, the path order.customer.profile.contact.address.shipping.city no longer exists:
{
"order": {
"id": "ord_9f2c",
"customer": {
"profile": {
"name": { "first": "Ava", "last": "Murphy" },
"contact": {
"email": "ava@example.com"
}
}
}
}
}
If your code assumes address.shipping.city is always present, it will crash when that optional section disappears.
In Python, that typically shows up as a KeyError (missing key) or a TypeError
(you expected a dictionary but hit None).
Issue 3: "Arrays multiply the problem"
Production APIs often return arrays containing hundreds of objects, and those objects don't always share the same shape.
One endpoint wraps records in a results array, another uses items,
and a third returns data directly at the root level with no wrapper at all.
Within those arrays, individual objects may vary too. Some records include optional fields, others omit them. Some fields are strings in one object and numbers in another. Some objects nest data deeply, others keep it flat.
Issue 4: "The same business objects can arrive in different shapes"
Now let's zoom out from individual field paths to full response shapes. Real APIs don't just nest fields deeply or omit optional data — sometimes the same business objects arrive in completely different shapes depending on which endpoint or partner system you're integrating with.
Below are two real responses from a Vendor Orders API. Both describe the same order by the same customer, but a modern endpoint and a legacy partner feed have serialised it differently. Before reading further, scan both responses and see how many differences you can spot. Focus on: field names, data types, nesting, and wrappers.
{
"id": "ORD-91352",
"created_at": "2025-06-18T14:22:31Z",
"total": "129.50",
"currency": "EUR",
"status": "shipped",
"customer": {
"id": 7712,
"email": "alice@example.com"
},
"items": [
{ "sku": "P-001", "qty": 2, "price": 39.75 },
{ "sku": "P-009", "qty": 1, "price": 50.00 }
],
"discount": null
}
This structure looks clean and predictable. Here's the same order from a legacy partner feed. The business concepts are identical — but watch what happens to the field names, types, and structure.
{
"order_id": 91352,
"ts": 1718710951,
"amount": 129.5,
"currency_code": "EUR",
"state": "Shipped",
"customer_id": "7712",
"line_items": [
{ "product": { "sku": "P-001" }, "quantity": "2", "unit_price": "39.75" },
{ "product": { "sku": "P-009" }, "quantity": 1, "unit_price": 50 }
],
"promo": { "code": "SUMMER", "value": "10%" }
}
Same order. Completely different shape. Here's what changed:
- Field naming:
idvsorder_id,totalvsamount,statusvsstate - Type differences:
totalas a string vsamountas a number;idas a string vs an integer - Date format: ISO 8601 string (
"2025-06-18T14:22:31Z") vs Unix timestamp (1718710951) - Customer data: embedded object with
idandemailvs a barecustomer_idreference string - Items array: flat fields (
sku,qty,price) vs nested product object with different key names - Discount: explicit
discount: nullvs apromoobject with a percentage string
This is the central challenge the chapter addresses. In Section 2 you'll examine these two variants in detail and design a target shape that both must produce. In Section 7 you'll build the normalizer that makes it happen. The tools and patterns in Sections 3–6 are what make that normalizer practical.
Our goal isn't to memorize every possible API structure. It's to build a systematic approach that works across JSON responses: diagnose the shape, normalize differences, navigate safely, and handle missing fields without crashing.
The chapter follows a deliberate progression: first you'll build exploration tools to understand what you're working with, then normalization utilities to create consistency, then safe accessors to navigate complexity, and finally defensive patterns to handle incomplete data. We won't abandon what you learned in Chapter 6. Instead, we'll extend it with professional patterns that handle the complexity real APIs actually present.
This chapter assumes you're comfortable with .json(), dictionary navigation with bracket notation and .get(),
and looping through arrays from Chapter 6. We'll extend these basics into professional-grade data handling patterns that prepare you for the comprehensive validation techniques in Chapter 12.
Tooling: Examples use Python 3.10+ and the requests library.
When You Need These Techniques
Before diving deep, let's be clear about scope: you won't need advanced JSON processing for every API integration. If you're working with a single, stable endpoint that won't change, accessing the raw response directly with basic .get() calls is perfectly fine.
This chapter teaches professional normalization patterns for specific situations where complexity is justified:
- Integrating multiple API variants: Different versions, different providers, or different endpoints with incompatible structures
- API structure changes frequently: The provider updates field names, nesting, or data types regularly
- Isolating business logic from API changes: You want your application code to stay stable even when external APIs evolve
- Building libraries or SDKs: You're wrapping external APIs for others to use
For a one-time data extraction script or a simple integration with a stable API, direct dictionary access is appropriate. Don't build infrastructure you don't need. The techniques in this chapter are tools, not mandates. Professional developers reach for normalization when the situation warrants it, not reflexively.
That said, understanding these patterns prepares you for the reality that even "simple" APIs often evolve into complex integrations. Learning the approach now means you'll recognize when it's time to apply it.
Chapter Roadmap
This chapter follows a three-phase progression: identify the problem, build the tools, then solve it. Here's the journey:
Mapping The Challenge
Using the Vendor Orders API as a case study, you'll learn how real APIs vary and how to plan a clean "target shape" before writing code.
Building Your Toolkit
Using GitHub's API as a practice ground, you'll build reusable helpers for exploring responses, extracting values safely, and handling missing or messy data without crashes.
Solving The Orders Challenge
You'll apply your toolkit to the Vendor Orders problem and build the normalizer step by step, using the Eight Transformation Patterns until both API variants produce identical output.
Key strategy: You'll build tools with GitHub's API first (Sections 3-6), then apply them in Section 7. This teaches transferable patterns, not one-off solutions.
Learning Objectives
By the end of this chapter, you'll be able to:
- Assess integration complexity: Recognize when API variation justifies building normalization layers versus using direct access.
- Systematic exploration: Use diagnostic tools to map unfamiliar API structures.
- Build canonical models: Transform varying API formats into consistent internal representations using the eight normalization patterns.
- Flexible access patterns: Extract data that works across different container shapes and preserves metadata.
- Deep JSON navigation: Navigate safely through nested objects and arrays without crashes.
- Defensive programming: Apply fail-fast and fail-soft strategies to handle missing or invalid data gracefully.