The first lesson ended with body.data.words, a single string sitting one level inside the envelope. Most responses are not that polite. Here is what the Country Lookup API returns for country=Japan:
{
"status": "ok",
"error": null,
"data": {
"search": "JAPAN",
"countries": [
{
"name": {
"common": "Japan",
"official": "Japan",
"native": {
"jpn": { "official": "日本", "common": "日本" }
}
},
"tld": [".jp", ".みんな"],
"cca2": "JP",
"ccn3": "392",
"cca3": "JPN",
"independent": true,
"capital": ["Tokyo"],
"region": "Asia",
"subregion": "Eastern Asia",
"languages": { "jpn": "Japanese" },
"latlng": [36, 138],
"landlocked": false,
"flag": "🇯🇵"
}
]
}
}
(A few fields are trimmed to fit.) Suppose all you want is the capital city. It is in there, four levels deep, inside two different lists. By the end of this page you will be able to look at a response like this and write the path to any value in it on the first try.
curl "https://api.apiverve.com/v1/countrylookup?country=Japan" \
-H "x-api-key: YOUR_API_KEY"Find the capital in the response before reading on. How many brackets do you pass through to get there?
01The six things a JSON value can be
JSON is plain text, but it describes a small, fixed set of value types. Learn to recognise them by sight and any response becomes readable.
| Type | Looks like | Example from the response above |
|---|---|---|
| String | Text in double quotes | "Japan", "392" |
| Number | Digits, no quotes | 36, 138 |
| Boolean | true or false, no quotes | "independent": true |
| Null | null, no quotes | "error": null in the envelope |
| Object | { } holding "key": value pairs | "name": { "common": ... } |
| Array | [ ] holding values in order | "capital": ["Tokyo"] |
Two details in that table trip people up.
"392" is a string, not a number. The quotes decide the type, not what the characters look like. ccn3 is a country's numeric ISO code, but it is sent as text because codes like "036" (Australia) have a leading zero that a number would throw away. If you compare country.ccn3 === 392 in JavaScript, it is always false. Compare with "392", or convert deliberately.
false and null are not the same as missing. "landlocked": false is a real answer: Japan is not landlocked. You will see later in this lesson why the difference between a value being false, null and absent is one of the most useful things to get right.
Objects and arrays are the two containers, and they are how nesting happens. An object holds named values, so you reach into it by name. An array holds values in order, so you reach into it by position, starting at zero.
In the Country Lookup response, what type is the value of "ccn3": "392"?
02Writing a path to any value
To reach a value, start at the top and name every container you pass through, in order. For the capital:
bodyis the whole envelope, an object..datais the object inside it..countriesis an array of matching countries.[0]is the first country in that array, an object..capitalis an array of capital cities.[0]is the first capital, the string"Tokyo".
Put together: body.data.countries[0].capital[0]. Dots for object keys, square brackets with a number for array positions. Python uses square brackets for both.
const country = body.data.countries[0];
console.log(country.name.common); // Japan
console.log(country.capital[0]); // Tokyo
console.log(country.latlng[1]); // 138
console.log(country.name.native.jpn.common); // 日本country = body["data"]["countries"][0]
print(country["name"]["common"]) # Japan
print(country["capital"][0]) # Tokyo
print(country["latlng"][1]) # 138
print(country["name"]["native"]["jpn"]["common"]) # 日本Pulling countries[0] into its own variable first is not just tidiness. It means the long path is written once, and every later line reads like a sentence about one country.
Why is capital a list?
You might expect capital to be a plain string. It is an array because some countries genuinely have more than one capital. Look up South Africa and you get ["Pretoria", "Bloemfontein", "Cape Town"]. The API uses the same shape for every country so your code does not need a special case. The same goes for tld, the country's internet domains: Japan has two, .jp and .みんな.
The lesson here is general. When a field is an array, treat it as "zero or more", even if every example you have seen has exactly one item. Writing capital[0] is fine when you want one value to display. Writing code that assumes capital[0] is the only capital is a bug waiting for the right input.
Arrays of results
The same applies one level up. countries is an array: the API answers with a list of matches, even when the list holds one country. Code that loops over the list keeps working whatever its length, and looping costs nothing:
for (const c of body.data.countries) {
console.log(`${c.name.common}: ${c.capital.join(", ")}`);
}What about no matches at all? You might expect an empty list. Search for Atlantis and you get something else: status is "error", data is null, the HTTP status is 404, and error says the country was not found. That is why the envelope check from lesson 1 comes before any path: on a failed lookup there is no countries array to index into. Lesson 5 covers what each status code means.
Which path reaches the official name of the first country in the Country Lookup response?
03Objects keyed by code
Look at languages:
"languages": { "jpn": "Japanese" }
This is an object, not an array, and the keys are language codes rather than fixed field names. You cannot write languages.name, because the key you need depends on the country. New Zealand's response has three keys: eng, mri and nzs.
When the keys are data rather than a fixed schema, you iterate over them instead of naming them:
const langs = body.data.countries[0].languages;
console.log(Object.keys(langs)); // [ 'jpn' ]
console.log(Object.values(langs)); // [ 'Japanese' ]
for (const [code, name] of Object.entries(langs)) {
console.log(code, "->", name);
}langs = body["data"]["countries"][0]["languages"]
print(list(langs.keys())) # ['jpn']
print(list(langs.values())) # ['Japanese']
for code, name in langs.items():
print(code, "->", name)The giveaway is in the reference page for an API: if a field is described as a map, a dictionary, or "keyed by" something, expect to loop over it. Try the lookup with a multilingual country to see the object grow:
curl "https://api.apiverve.com/v1/countrylookup?country=New+Zealand" \
-H "x-api-key: YOUR_API_KEY"Look at languages and name.native. Both are objects keyed by language code, and both have three keys here.
04null versus missing
This is the part of reading JSON that separates code that works in a demo from code that works in production.
A field can be in one of three states:
- Present with a value, like
"landlocked": false. - Present and
null, meaning the API is telling you, explicitly, that there is no value. - Absent, meaning the key is not in the object at all.
Number to Words shows the second state cleanly. Call it with a whole number:
{
"number": 4021,
"words": "four thousand, twenty-one",
"afterDecimal": null
}
Then with 12.5:
{
"number": 12.5,
"words": "twelve point five",
"afterDecimal": {
"number": "5",
"words": ["five"],
"numberOfDigits_numeric": 1,
"numberOfDigits_words": "one"
}
}
curl "https://api.apiverve.com/v1/numbertowords?number=12.5" \
-H "x-api-key: YOUR_API_KEY"afterDecimal is an object here. Change the number to 12 and it becomes null.
afterDecimal is always present, but it is null when there is nothing after the decimal point. So data.afterDecimal.words works for 12.5 and throws TypeError: Cannot read properties of null for 4021. The fix is to check before you step into it:
const decimals = body.data.afterDecimal?.words ?? [];The ?. (optional chaining) stops and returns undefined if the thing on its left is null or undefined, instead of crashing. The ?? then supplies a default. Python has no ?., so you write the check out:
after = body["data"]["afterDecimal"]
decimals = after["words"] if after is not None else []Premium fields come back as null
The second state comes up on APIVerve in a specific, documented way. Some fields are premium, and fields your plan does not include come back as null: the key is there, the value is not, and it is never filled in with 0 or an empty string. The Email Validator riskScore field is the standard example, and the response format docs explain the reasoning: a risk score of 0 means "safe", so treating that null as 0 would be exactly the wrong default. data.riskScore ?? 0 turns "not on your plan" into "no risk".
When a field is absent
The third state is rarer, but you will meet it. Some APIs leave out parts they did not find (Street Address Parser only returns the address components that were in the input), and a misspelt field name looks exactly like a missing one. In code, an absent key reads as undefined in JavaScript and raises KeyError in Python if you use square brackets:
data.get("riskScore") # None if null or absent, no exception
data["riskScore"] # None if null, KeyError if absent
"riskScore" in data # True even when the value is nullThe rule to take away: test for a value before you use it, and never fill the gap with a meaningful default. if (data.riskScore === 0) is wrong when the field might be null or missing, because null === 0 and undefined === 0 are both false and the code quietly takes the other branch. In JavaScript, data.riskScore == null is true for both null and undefined, so if (data.riskScore == null) catches a premium field your plan does not include and a key that is not there at all. In Python, data.get("riskScore") is None does the same job.
You call Number to Words with number=4021 and run data.afterDecimal.words. What happens?
05Printing a response so you can read it
When you are working out a path, look at the real response rather than guessing from memory. Two quick ways to make it readable:
curl -s 'https://api.apiverve.com/v1/countrylookup?country=Japan' \
-H "x-api-key: $APIVERVE_API_KEY" | python -m json.tool-s hides curl's progress bar, and python -m json.tool reformats the JSON with indentation. If you have jq installed, | jq '.data.countries[0].capital' goes further and pulls out just the path you name, which is a good way to test a path before you write it in code.
In Node, console.log cuts off deeply nested objects with [Object]. Use this instead:
console.log(JSON.stringify(body.data, null, 2));The 2 is the indentation. It prints every level, so nothing is hidden behind [Object].
06What you built, and what's next
You can now name the six JSON types on sight, write a path through nested objects and arrays, loop over arrays of results and over objects keyed by code, and tell a null field from an absent one, with code that handles both without crashing.
So far you have been sending your API key along without thinking much about it. The next lesson, API keys and authentication, covers what that key really is, what the server says when it is missing or wrong, and the handful of rules that stop it from leaking.