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?
The 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"?
Answer the check above to continue.
Objects 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.
null 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 []When a field is absent
The third state comes up on APIVerve in a specific, documented way. Some fields are premium, and fields your plan does not include are left out of the response entirely, not sent as null or 0. 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 inventing a 0 for a field you did not receive would be exactly the wrong default.
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 absent, no exception
data["riskScore"] # KeyError if absent
"riskScore" in data # the honest test: is it there at all?The rule to take away: test for presence before you test the value. if (data.riskScore === 0) is wrong when the field might be missing, because undefined === 0 is false and the code quietly takes the other branch. if ("riskScore" in data) asks the right question first.
You call Number to Words with number=4021 and run data.afterDecimal.words. What happens?
Printing 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].
What 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.